diff --git a/src/macaron/console.py b/src/macaron/console.py
index 10a624dd4..b81ad92f6 100644
--- a/src/macaron/console.py
+++ b/src/macaron/console.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2025 - 2025, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2025 - 2026, Oracle and/or its affiliates. All rights reserved.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
"""This module implements a rich console handler for logging."""
@@ -306,6 +306,7 @@ def __init__(self, *args: Any, verbose: bool = False, **kwargs: Any) -> None:
self.policies_table = Table(box=None)
self.components_violates_table = Table(box=None)
self.components_satisfy_table = Table(box=None)
+ self.policy_evidence_table = Table(box=None)
self.policy_summary_table = Table(show_header=False, box=None)
self.policy_summary: dict[str, str | Status] = {
"Passed Policies": "None",
@@ -537,7 +538,7 @@ def update_policy_engine(self, results: dict) -> None:
components_violates_table.add_column("PURL", justify="left")
components_violates_table.add_column("Policy Name", justify="left")
- for values in results["component_violates_policy"]:
+ for values in results.get("component_violates_policy", []):
components_violates_table.add_row(values[0], values[1], values[2])
self.components_violates_table = components_violates_table
@@ -547,16 +548,37 @@ def update_policy_engine(self, results: dict) -> None:
components_satisfy_table.add_column("PURL", justify="left")
components_satisfy_table.add_column("Policy Name", justify="left")
- for values in results["component_satisfies_policy"]:
+ for values in results.get("component_satisfies_policy", []):
components_satisfy_table.add_row(values[0], values[1], values[2])
self.components_satisfy_table = components_satisfy_table
+ standard_outputs = {
+ "passed_policies",
+ "failed_policies",
+ "component_satisfies_policy",
+ "component_violates_policy",
+ }
+ policy_evidence_table = Table(box=None)
+ policy_evidence_table.add_column("Relation", justify="left")
+ policy_evidence_table.add_column("Evidence", justify="left")
+ for relation, rows in sorted(results.items()):
+ if relation in standard_outputs:
+ continue
+ for values in rows:
+ policy_evidence_table.add_row(relation, " | ".join(values))
+
+ self.policy_evidence_table = policy_evidence_table
+
self.policy_summary["Passed Policies"] = (
- "\n".join(policy[0] for policy in results["passed_policies"]) if results["passed_policies"] else "None"
+ "\n".join(policy[0] for policy in results.get("passed_policies", []))
+ if results.get("passed_policies", [])
+ else "None"
)
self.policy_summary["Failed Policies"] = (
- "\n".join(policy[0] for policy in results["failed_policies"]) if results["failed_policies"] else "None"
+ "\n".join(policy[0] for policy in results.get("failed_policies", []))
+ if results.get("failed_policies", [])
+ else "None"
)
self.generate_policy_summary_table()
@@ -756,6 +778,12 @@ def make_layout(self) -> Group:
"[bold red] Components Violate Policy[/] [white not italic]None[/]",
]
layout = layout + ["", self.policy_summary_table]
+ if self.policy_evidence_table.row_count > 0:
+ layout = layout + [
+ "",
+ "[bold yellow] Policy Evidence[/]",
+ self.policy_evidence_table,
+ ]
if self.verification_summary_attestation:
vsa_table = Table(show_header=False, box=None)
vsa_table.add_column("Detail", justify="left")
diff --git a/src/macaron/dependency_analyzer/cyclonedx.py b/src/macaron/dependency_analyzer/cyclonedx.py
index c46a8a773..eedcdb09e 100644
--- a/src/macaron/dependency_analyzer/cyclonedx.py
+++ b/src/macaron/dependency_analyzer/cyclonedx.py
@@ -1,4 +1,4 @@
-# Copyright (c) 2023 - 2025, Oracle and/or its affiliates. All rights reserved.
+# Copyright (c) 2023 - 2026, Oracle and/or its affiliates. All rights reserved.
# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
"""This module contains helper functions to process CycloneDX SBOM."""
@@ -357,10 +357,9 @@ def resolve_dependencies(main_ctx: Any, sbom_path: str, recursive: bool = False)
# Start resolving dependencies.
logger.info(
- "Running %s version %s dependency analyzer on %s",
+ "Running %s version %s dependency analyzer.",
dep_analyzer.tool_name,
dep_analyzer.tool_version,
- find_report_output_path(main_ctx.component.repository.fs_path),
)
log_path = os.path.join(
diff --git a/src/macaron/output_reporter/malware_summary.py b/src/macaron/output_reporter/malware_summary.py
new file mode 100644
index 000000000..2b46383d1
--- /dev/null
+++ b/src/macaron/output_reporter/malware_summary.py
@@ -0,0 +1,175 @@
+# Copyright (c) 2026 - 2026, Oracle and/or its affiliates. All rights reserved.
+# Licensed under the Universal Permissive License v 1.0 as shown at https://oss.oracle.com/licenses/upl/.
+
+"""Helpers for building user-friendly malware findings for output reports."""
+
+from __future__ import annotations
+
+from typing import Any
+
+from macaron.slsa_analyzer.checks.check_result import CheckResult, CheckResultType
+
+MALICIOUS_METADATA_CHECK_ID = "mcn_detect_malicious_metadata_1"
+
+_HEURISTIC_REASONS = {
+ "empty_project_link": "Package metadata has no useful project links.",
+ "source_code_repo": "Macaron could not identify a matching source repository.",
+ "one_release": "The package has only one release.",
+ "high_release_frequency": "The package has an unusual release cadence.",
+ "unchanged_release": "Multiple releases appear to contain unchanged code.",
+ "closer_release_join_date": "A maintainer joined close to the release time.",
+ "suspicious_setup": "The package contains suspicious setup/install behavior.",
+ "wheel_absence": "The package omits a wheel, which can force source install behavior.",
+ "anomalous_version": "The package version looks unusual for the package history.",
+ "typosquatting_presence": "The package name resembles another package.",
+ "fake_email": "Maintainer metadata contains suspicious email information.",
+ "similar_projects": "Macaron found similarly structured package projects.",
+ "package_description_intent": "The package description does not clearly explain benign intent.",
+ "type_stub_file": "The package does not look like a legitimate type-stub package.",
+ "suspicious_patterns": "Source code analysis found suspicious Python malware patterns.",
+}
+
+_CATEGORY_KEYWORDS = (
+ ("Command and control", ("c2", "domain", "url", "endpoint", "network", "exfil")),
+ ("Payload staging", ("payload", "download", "rope", "pyz", "dropper")),
+ ("Persistence", ("persist", "systemd", "service", "unit")),
+ ("Credential access", ("credential", "secret", "token", "aws", "azure", "gcp", "kubernetes", "vault", "ssh")),
+ ("Suspicious execution", ("obfuscation", "inline", "import", "exec", "eval", "system", "subprocess", "command")),
+)
+
+
+def _display_value(value: Any) -> Any:
+ """Return a JSON-serializable display value for enums and primitive values."""
+ if hasattr(value, "value"):
+ return value.value
+ return value
+
+
+def _confidence_value(value: Any) -> float | None:
+ """Return a primitive confidence score."""
+ display_value = _display_value(value)
+ if isinstance(display_value, (int, float)):
+ return float(display_value)
+ return None
+
+
+def _categorize_source_finding(rule_id: str, message: str) -> str:
+ """Map a source-code rule finding to a user-facing behavior category."""
+ haystack = f"{rule_id} {message}".lower()
+ for category, keywords in _CATEGORY_KEYWORDS:
+ if any(keyword in haystack for keyword in keywords):
+ return category
+ return "Suspicious source pattern"
+
+
+def _build_failed_heuristics(result: dict[Any, Any]) -> list[dict[str, str]]:
+ """Build a readable list of failed heuristics."""
+ failed_heuristics = []
+ for heuristic, heuristic_result in result.items():
+ heuristic_name = str(_display_value(heuristic))
+ status = str(_display_value(heuristic_result))
+ if status != "FAIL":
+ continue
+ failed_heuristics.append(
+ {
+ "name": heuristic_name,
+ "reason": _HEURISTIC_REASONS.get(heuristic_name, "This heuristic flagged suspicious package behavior."),
+ }
+ )
+ return failed_heuristics
+
+
+def _build_behavioral_findings(detail_information: dict[str, Any]) -> list[dict[str, Any]]:
+ """Build behavior-oriented findings from source-code rule evidence."""
+ source_findings = detail_information.get("enabled_sourcecode_rule_findings")
+ if not isinstance(source_findings, dict):
+ return []
+
+ grouped_findings: dict[str, dict[str, Any]] = {}
+ for rule_id, finding in source_findings.items():
+ if not isinstance(finding, dict):
+ continue
+ message = finding.get("message", "Source-code rule matched suspicious code.")
+ if not isinstance(message, str):
+ message = "Source-code rule matched suspicious code."
+
+ category = _categorize_source_finding(rule_id, message)
+ grouped = grouped_findings.setdefault(
+ category,
+ {
+ "category": category,
+ "reason": message,
+ "rules": [],
+ "evidence": [],
+ },
+ )
+ grouped["rules"].append(rule_id)
+ detections = finding.get("detections", [])
+ if isinstance(detections, list):
+ grouped["evidence"].extend(detection for detection in detections if isinstance(detection, dict))
+
+ return list(grouped_findings.values())
+
+
+def _recommended_actions(verdict: str, behavioral_findings: list[dict[str, Any]]) -> list[str]:
+ """Return concise next steps for a malware finding."""
+ if verdict == "no_suspicious_indicators":
+ return []
+
+ actions = [
+ "Block the package from promotion until the finding is reviewed.",
+ "Send the package artifact to dynamic analysis if available.",
+ ]
+ categories = {finding["category"] for finding in behavioral_findings}
+ if "Credential access" in categories:
+ actions.append("Investigate whether systems that imported this package exposed credentials.")
+ if "Command and control" in categories:
+ actions.append("Review network controls for the reported domains or endpoints.")
+ return actions
+
+
+def build_malware_summary(check_result: CheckResult) -> dict[str, Any] | None:
+ """Build a user-friendly malware summary for the malicious metadata check."""
+ if check_result.check.check_id != MALICIOUS_METADATA_CHECK_ID:
+ return None
+ if check_result.result.result_type != CheckResultType.FAILED:
+ return None
+
+ summaries = []
+ for result_table in check_result.result.result_tables:
+ result = getattr(result_table, "result", {}) or {}
+ detail_information = getattr(result_table, "detail_information", {}) or {}
+ known_malware = getattr(result_table, "known_malware", None)
+ confidence = _confidence_value(getattr(result_table, "confidence", None))
+
+ failed_heuristics = _build_failed_heuristics(result)
+ behavioral_findings = _build_behavioral_findings(detail_information)
+
+ if known_malware:
+ verdict = "known_malware"
+ primary_reason = f"Package is listed as known malware: {known_malware}"
+ else:
+ verdict = "suspicious"
+ if behavioral_findings:
+ primary_reason = "Source code contains suspicious Python malware patterns."
+ elif failed_heuristics:
+ primary_reason = "Package metadata matches suspicious malware heuristics."
+ else:
+ primary_reason = "Macaron's malicious package check failed."
+
+ summaries.append(
+ {
+ "check_id": check_result.check.check_id,
+ "verdict": verdict,
+ "confidence": confidence,
+ "primary_reason": primary_reason,
+ "known_malware": known_malware,
+ "failed_heuristics": failed_heuristics,
+ "behavioral_findings": behavioral_findings,
+ "recommended_actions": _recommended_actions(verdict, behavioral_findings),
+ }
+ )
+
+ if not summaries:
+ return None
+ return summaries[0]
diff --git a/src/macaron/output_reporter/templates/base_template.html b/src/macaron/output_reporter/templates/base_template.html
index 0d15b35a9..9bd710ccd 100644
--- a/src/macaron/output_reporter/templates/base_template.html
+++ b/src/macaron/output_reporter/templates/base_template.html
@@ -22,14 +22,23 @@
body {
margin-bottom: 20px;
+ background:
+ linear-gradient(180deg, #f8fafc 0, #ffffff 360px),
+ #ffffff;
+ color: #22302f;
}
a {
- color: #045fab;
+ color: #0f766e;
line-height: inherit;
text-decoration: none;
}
+ a:hover {
+ color: #0b5f59;
+ text-decoration: underline;
+ }
+
pre {
padding-left: 10px;
font-size: large;
@@ -38,37 +47,61 @@
button {
border: 0;
- padding: 0;
- padding-top: 10px;
- padding-bottom: 10px;
- background: lightgrey;
+ padding: 10px 14px;
+ background: #e2e8f0;
box-shadow: none;
- border-radius: 0px;
+ border-radius: 8px;
}
button:hover:enabled,
.tablebutton_current {
- background: grey;
+ background: #0f766e;
color: white;
}
table,
th,
td {
- padding: 10px;
- border: 1px black solid;
+ padding: 11px 12px;
+ border: 1px solid #dbe5e3;
border-collapse: collapse;
box-sizing: border-box;
}
+ table {
+ background: #ffffff;
+ color: #22302f;
+ }
+
+ thead th {
+ background: #0f3f3d;
+ border-color: #0f3f3d;
+ color: #f8fafc;
+ font-size: 13px;
+ letter-spacing: 0.03em;
+ text-transform: uppercase;
+ }
+
th {
min-width: fit-content;
text-align: left;
+ background: #f8fafc;
+ color: #26403e;
+ font-weight: 800;
}
td {
word-wrap: break-word;
word-break: keep-all;
+ line-height: 1.45;
+ }
+
+ tbody tr:nth-child(even) td {
+ background: #fbfdfc;
+ }
+
+ tbody tr:hover td {
+ background: #f0fdfa;
}
th a:active {
@@ -88,6 +121,8 @@
footer {
margin-top: 20px;
text-align: center;
+ color: #64748b;
+ font-size: 13px;
}
.raw {
@@ -110,22 +145,37 @@
#scroll_to_top {
position: fixed;
- padding: 20px;
+ width: auto;
+ padding: 14px 16px;
bottom: 20px;
right: 30px;
font-size: 20px;
z-index: 99;
+ border: 1px solid rgba(15, 118, 110, 0.24);
+ border-radius: 999px;
+ background: #0f766e;
+ color: white;
+ box-shadow: 0 10px 26px rgba(15, 118, 110, 0.28);
}
.space_divider {
- height: 20px;
+ height: 16px;
}
#report_title {
- padding: 10px 10px;
+ width: auto;
+ margin: 16px;
+ padding: 24px 20px;
+ border: 1px solid #cfe7e3;
+ border-radius: 8px;
+ background:
+ linear-gradient(135deg, rgba(15, 63, 61, 0.97), rgba(15, 118, 110, 0.94) 55%, rgba(127, 29, 29, 0.94)),
+ #0f3f3d;
+ box-shadow: 0 14px 36px rgba(15, 63, 61, 0.16);
+ color: white;
font-weight: bold;
text-align: center;
- font-size: 40px;
+ font-size: 38px;
display: flex;
flex-direction: column;
}
@@ -133,64 +183,81 @@
#macaron-svg {
align-self: center;
padding: 10px;
+ filter: drop-shadow(0 8px 14px rgba(0, 0, 0, 0.22));
}
.red_bg {
- background-color: red;
+ background-color: #dc2626;
color: white;
text-align: center;
font-weight: bold;
}
.green_bg {
- background-color: forestgreen;
+ background-color: #16a34a;
color: white;
text-align: center;
font-weight: bold;
}
.lightgreen_bg {
- background-color: lightgreen;
- color: grey;
+ background-color: #dcfce7;
+ color: #166534;
text-align: center;
font-weight: bold;
}
.grey_bg {
- background-color: grey;
- color: black;
+ background-color: #e2e8f0;
+ color: #475569;
text-align: center;
font-weight: bold;
}
.black_bg {
- background-color: black;
- color: slategray;
+ background-color: #0f172a;
+ color: #cbd5e1;
text-align: center;
font-weight: bold;
}
.timestamps {
+ width: auto;
padding-top: 10px;
padding-bottom: 10px;
border: 0;
- font-size: small;
- font-weight: lighter;
+ color: #d9fff9;
+ font-size: 13px;
+ font-weight: 500;
text-align: center;
}
.table_caption {
- text-align: center;
- font-weight: bold;
- padding: 10px 10px;
- font-size: 25px;
+ box-sizing: border-box;
+ width: auto;
+ margin-bottom: 14px;
+ padding: 14px 16px;
+ border-left: 5px solid #0f766e;
+ border-radius: 8px;
+ background: linear-gradient(90deg, #ecfdf5, #ffffff);
+ color: #0f3f3d;
+ text-align: left;
+ font-weight: 800;
+ font-size: 23px;
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.8);
}
.table_sub_caption {
- text-align: center;
- font-weight: bold;
- padding: 10px 10px;
- font-size: 18px;
+ box-sizing: border-box;
+ width: auto;
+ margin: 14px 0 10px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ background: #f8fafc;
+ color: #334155;
+ text-align: left;
+ font-weight: 800;
+ font-size: 16px;
}
#mcn_prov_sum {
@@ -198,9 +265,18 @@
}
#prov_justification {
- text-align: center;
- font-style: italic;
- font-size: 18px;
+ box-sizing: border-box;
+ width: auto;
+ margin: 0 0 14px;
+ padding: 12px 14px;
+ border: 1px solid #d9e5e2;
+ border-radius: 8px;
+ background: #ffffff;
+ color: #475569;
+ text-align: left;
+ font-style: normal;
+ font-size: 15px;
+ line-height: 1.45;
}
/* Format the Slsa requirements column.
@@ -210,33 +286,99 @@
white-space: nowrap;
}
+ .report-section {
+ box-sizing: border-box;
+ width: calc(100% - 32px);
+ margin: 16px;
+ padding: 16px;
+ border: 1px solid #d9e5e2;
+ border-radius: 8px;
+ background:
+ linear-gradient(180deg, rgba(255, 255, 255, 0.96), rgba(248, 250, 252, 0.96)),
+ #ffffff;
+ box-shadow: 0 10px 28px rgba(15, 63, 61, 0.08);
+ overflow-x: auto;
+ }
+
+ .report-section table {
+ width: 100%;
+ margin: 0;
+ border: 1px solid #dbe5e3;
+ border-radius: 8px;
+ border-collapse: separate;
+ border-spacing: 0;
+ overflow: hidden;
+ }
+
+ .report-section table th:first-child,
+ .report-section table td:first-child {
+ border-left: 0;
+ }
+
+ .report-section table th:last-child,
+ .report-section table td:last-child {
+ border-right: 0;
+ }
+
+ .report-section table tr:first-child th,
+ .report-section table tr:first-child td {
+ border-top: 0;
+ }
+
+ .report-section table tr:last-child th,
+ .report-section table tr:last-child td {
+ border-bottom: 0;
+ }
+
+ .target_info th {
+ width: 220px;
+ background: #f0fdfa;
+ color: #0f3f3d;
+ }
+
+ .checks_report td:first-child,
+ .dep_summary td:first-child,
+ .dep_status td:first-child {
+ font-weight: 700;
+ color: #0f3f3d;
+ }
+
+ .report-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 10px;
+ width: auto;
+ margin-bottom: 12px;
+ }
+
/* Customize the jquery-bonsai expand-all/collapse-all buttons. */
.fancy-button {
- border-radius: 6px;
box-sizing: border-box;
- color: black;
+ width: auto;
+ color: #0f3f3d;
cursor: pointer;
display: inline-block;
- font-weight: 500;
+ font-weight: 800;
line-height: 20px;
list-style: none;
- padding: 6px 16px;
+ padding: 9px 14px;
position: relative;
user-select: none;
vertical-align: middle;
white-space: nowrap;
word-wrap: break-word;
- font-weight: bold;
- border: 2px solid #d5d3d3;
- border-radius: 10px;
+ border: 1px solid #99d5cd;
+ border-radius: 8px;
+ background: #ecfdf5;
vertical-align: middle;
white-space: nowrap;
word-wrap: break-word;
- margin: 10px;
+ margin: 0;
}
.fancy-button:hover {
- background-color: grey;
+ background-color: #0f766e;
+ color: white;
text-decoration: none;
transition-duration: 0.1s;
}
@@ -248,11 +390,26 @@
.tree-view-nested-list, .tree-view-nested-list ul {
/* Remove default bullets. This will be replaced with a caret. */
list-style-type: none;
- padding: 0
+ box-sizing: border-box;
+ width: auto;
+ padding: 0;
}
.tree-view-nested-list li {
- padding-left: 2em
+ padding-left: 1.4em;
+ line-height: 1.65;
+ }
+
+ .report-section > .tree-view-nested-list,
+ #run-checks-tree > .tree-view-nested-list {
+ padding: 14px;
+ border: 1px solid #d9e5e2;
+ border-radius: 8px;
+ background: #ffffff;
+ }
+
+ .tree-view-nested-list div {
+ width: auto;
}
/*
@@ -267,7 +424,7 @@
/* Create the caret using a unicode symbol, and style it. */
.caret::before, .toggler::before {
content: "\25B8";
- color: black;
+ color: #0f766e;
display: inline-block;
margin-right: 6px;
}
@@ -289,10 +446,340 @@
.check-tree-node::before {
content: "\2514";
- color: black;
+ color: #94a3b8;
display: inline-block;
margin-right: 6px;
}
+
+ .malware-summary {
+ box-sizing: border-box;
+ width: calc(100% - 32px);
+ margin: 16px;
+ border: 1px solid #f2b6a6;
+ border-radius: 8px;
+ background:
+ linear-gradient(135deg, rgba(255, 244, 239, 0.98), rgba(255, 255, 255, 0.98) 46%, rgba(240, 250, 248, 0.98)),
+ repeating-linear-gradient(135deg, rgba(190, 18, 60, 0.05) 0 2px, transparent 2px 12px);
+ box-shadow: 0 10px 28px rgba(108, 35, 28, 0.13);
+ color: #2f201f;
+ overflow: hidden;
+ }
+
+ .malware-summary * {
+ box-sizing: border-box;
+ }
+
+ .malware-summary-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 16px;
+ padding: 18px 20px;
+ background: linear-gradient(90deg, #7f1d1d, #b91c1c 48%, #0f766e);
+ color: white;
+ }
+
+ .malware-summary-header > div:first-child {
+ width: auto;
+ min-width: 0;
+ flex: 1 1 auto;
+ }
+
+ .malware-summary-title {
+ width: auto;
+ font-size: 23px;
+ font-weight: 800;
+ line-height: 1.2;
+ }
+
+ .malware-summary-reason {
+ width: auto;
+ max-width: 920px;
+ margin-top: 6px;
+ color: #ffe7df;
+ font-size: 15px;
+ line-height: 1.45;
+ }
+
+ .malware-verdict-badge {
+ width: auto;
+ flex: 0 0 auto;
+ padding: 8px 12px;
+ border: 1px solid rgba(255, 255, 255, 0.45);
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.17);
+ color: #fff7ed;
+ font-size: 13px;
+ font-weight: 800;
+ letter-spacing: 0.04em;
+ text-transform: uppercase;
+ white-space: nowrap;
+ }
+
+ .malware-summary-grid {
+ display: grid;
+ grid-template-columns: minmax(240px, 0.9fr) minmax(360px, 1.6fr);
+ gap: 16px;
+ padding: 16px;
+ }
+
+ .malware-panel {
+ width: auto;
+ border: 1px solid #f2d1c5;
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.86);
+ box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.75);
+ overflow: hidden;
+ }
+
+ .malware-summary-grid > .malware-panel:only-child {
+ grid-column: 1 / -1;
+ }
+
+ .malware-panel-wide {
+ margin: 0 16px 16px;
+ }
+
+ .malware-panel-title {
+ width: auto;
+ padding: 12px 14px;
+ background: #fff7ed;
+ color: #7f1d1d;
+ font-size: 14px;
+ font-weight: 800;
+ text-transform: uppercase;
+ }
+
+ .malware-panel-body {
+ padding: 14px;
+ }
+
+ .malware-stat {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 10px;
+ width: auto;
+ margin-bottom: 12px;
+ color: #4b2c28;
+ font-size: 14px;
+ }
+
+ .malware-stat strong {
+ width: auto;
+ font-size: 22px;
+ color: #991b1b;
+ }
+
+ .malware-meter {
+ width: 100%;
+ height: 13px;
+ border-radius: 999px;
+ background: #fee2e2;
+ border: 1px solid #fecaca;
+ overflow: hidden;
+ }
+
+ .malware-meter-fill {
+ height: 100%;
+ border-radius: inherit;
+ background: linear-gradient(90deg, #f97316, #dc2626);
+ }
+
+ .malware-link {
+ display: block;
+ margin-top: 12px;
+ padding: 10px 12px;
+ border-radius: 8px;
+ background: #eff6ff;
+ border: 1px solid #bfdbfe;
+ color: #075985;
+ font-weight: 700;
+ overflow-wrap: anywhere;
+ }
+
+ .malware-card-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+ gap: 12px;
+ }
+
+ .malware-card {
+ width: auto;
+ min-height: 132px;
+ padding: 14px;
+ border: 1px solid #fde2d2;
+ border-left: 5px solid #dc2626;
+ border-radius: 8px;
+ background: linear-gradient(180deg, #ffffff, #fff7ed);
+ }
+
+ .malware-card-heading {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 8px;
+ width: auto;
+ margin-bottom: 8px;
+ color: #7f1d1d;
+ font-size: 16px;
+ font-weight: 800;
+ }
+
+ .malware-rule-count {
+ width: auto;
+ padding: 4px 8px;
+ border-radius: 999px;
+ background: #fee2e2;
+ color: #991b1b;
+ font-size: 12px;
+ font-weight: 800;
+ white-space: nowrap;
+ }
+
+ .malware-card p {
+ width: auto;
+ margin-bottom: 10px;
+ color: #51302a;
+ line-height: 1.4;
+ }
+
+ .malware-evidence-list {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ width: auto;
+ padding: 0;
+ list-style: none;
+ }
+
+ .malware-evidence-list li {
+ max-width: 100%;
+ padding: 5px 8px;
+ border-radius: 999px;
+ background: #f8fafc;
+ border: 1px solid #cbd5e1;
+ color: #334155;
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
+ font-size: 12px;
+ overflow-wrap: anywhere;
+ }
+
+ .malware-evidence-list li.malware-evidence-more {
+ width: 100%;
+ padding: 0;
+ border: 0;
+ border-radius: 8px;
+ background: transparent;
+ flex-basis: 100%;
+ }
+
+ .malware-evidence-more details {
+ width: auto;
+ }
+
+ .malware-evidence-more summary {
+ display: inline-block;
+ width: auto;
+ padding: 5px 9px;
+ border: 1px solid #99d5cd;
+ border-radius: 999px;
+ background: #ecfdf5;
+ color: #0f766e;
+ cursor: pointer;
+ font-family: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
+ font-size: 12px;
+ font-weight: 800;
+ list-style: none;
+ }
+
+ .malware-evidence-more summary::-webkit-details-marker {
+ display: none;
+ }
+
+ .malware-evidence-more summary::before {
+ content: "\25B8";
+ display: inline-block;
+ margin-right: 5px;
+ transition: transform 0.12s ease-in-out;
+ }
+
+ .malware-evidence-more details[open] summary {
+ margin-bottom: 8px;
+ background: #0f766e;
+ color: white;
+ }
+
+ .malware-evidence-more details[open] summary::before {
+ transform: rotate(90deg);
+ }
+
+ .malware-evidence-list-extra {
+ margin-top: 0;
+ }
+
+ .malware-heuristic-list,
+ .malware-action-list {
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
+ gap: 10px;
+ width: auto;
+ padding: 0;
+ list-style: none;
+ }
+
+ .malware-heuristic-list li,
+ .malware-action-list li {
+ padding: 12px;
+ border-radius: 8px;
+ border: 1px solid #d9e5e2;
+ background: #ffffff;
+ color: #2f3b3a;
+ line-height: 1.4;
+ }
+
+ .malware-heuristic-list strong {
+ display: block;
+ width: auto;
+ margin-bottom: 4px;
+ color: #0f766e;
+ }
+
+ .malware-action-list li {
+ border-left: 5px solid #0f766e;
+ background: linear-gradient(90deg, #ecfdf5, #ffffff);
+ }
+
+ @media (max-width: 850px) {
+ #report_title,
+ .report-section,
+ .malware-summary {
+ width: calc(100% - 20px);
+ margin: 10px;
+ }
+
+ .report-section {
+ padding: 12px;
+ }
+
+ .table_caption {
+ font-size: 20px;
+ }
+
+ .malware-summary-header,
+ .malware-summary-grid {
+ display: block;
+ }
+
+ .malware-verdict-badge {
+ display: inline-block;
+ margin-top: 12px;
+ }
+
+ .malware-panel {
+ margin-top: 12px;
+ }
+ }
diff --git a/src/macaron/output_reporter/templates/macaron.html b/src/macaron/output_reporter/templates/macaron.html
index a20f4edd7..11092283e 100644
--- a/src/macaron/output_reporter/templates/macaron.html
+++ b/src/macaron/output_reporter/templates/macaron.html
@@ -128,6 +128,120 @@
{% endcall %}
{% endmacro %}
+{% macro render_malware_summary(malware_summary) %}
+{% set confidence_percent = (malware_summary.confidence * 100) | round | int if malware_summary.confidence is not none else none %}
+ {{ finding.reason }}
+ {% for detection in finding.evidence[:5] %}
+
+ {{ finding.evidence | length - 5 }} more finding{{ "s" if finding.evidence | length - 5 != 1 else "" }}
+
+ {% for detection in finding.evidence[5:] %}
+
+
+ {% for heuristic in malware_summary.failed_heuristics %}
+
+
+ {% for action in malware_summary.recommended_actions %}
+
+