Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
28c57c8
fix(oras-scan): per-worker Trivy cache pre-populated from the shared DB
Apr 17, 2026
e926dc3
perf(cosign-scan): skip cosign verify for images with no referrers
Apr 17, 2026
aec6693
fix(cosign-scan): only fast-path UNSIGNED on definitive discover result
Apr 17, 2026
06845f3
fix(cosign-verify): pass --new-bundle-format so OCI 1.1 sigs are found
Apr 17, 2026
96b5d45
fix(cosign-verify): bump verify_timeout to 180s for heavily-attested …
Apr 17, 2026
d55a44a
fix(cosign-scan): drop redundant cosign verify, use referrers as sour…
Apr 17, 2026
7b96d4d
perf(cosign-scan): read predicate type from referrer annotation
Apr 17, 2026
3c86ca5
fix(cosign-scan): revert annotation shortcut, raise predicate decode …
Apr 17, 2026
29a0660
perf(cosign-scan): early-terminate predicate enumeration when classif…
Apr 17, 2026
a8005d3
perf(cosign-scan): cache predicate->digests map for extract_sbom / ex…
Apr 17, 2026
66a98a2
diag(cosign-scan): surface predicate map + discovery_failed at INFO l…
Apr 17, 2026
2e99a65
diag(cosign-scan): emit UNSIGNED diagnostics via stderr print
Apr 17, 2026
6450a35
fix(cosign-scan): raise Referrers API timeout + tag-based fallback
Apr 17, 2026
96aaeee
feat(reports): sort images alphabetically + replace "Namespace" label
Apr 17, 2026
b7b613b
feat(reports): drop project segment from image column in tables
Apr 20, 2026
1ecba01
fix(reports): sort image rows by basename to match displayed column
Apr 20, 2026
b0ede86
fix(trivy): prefer ghcr trivy-db over default mirror
Apr 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions scanner_py/cli/generate_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,10 @@ def generate_markdown_report(
images_with_chainguard = 0
displayed_count = 0

for analysis in cve_analysis:
for analysis in sorted(
cve_analysis,
key=lambda a: a.get("image", "").rsplit("/", 1)[-1].lower(),
):
# Get CVE counts
critical = analysis.get("critical", 0)
high = analysis.get("high", 0)
Expand Down Expand Up @@ -356,10 +359,14 @@ def generate_markdown_report(

displayed_count += 1

# Format image name
image_short = analysis["image"].split("/")[-1]
if len(image_short) > 40:
image_short = image_short[:37] + "..."
# Show only the image basename (`<image>:<tag>`); strip registry
# host and project segments. The project prefix is identical for
# most rows in a typical chart-wide scan and adds noise without
# signal. Truncate very long refs (> 50 chars) by clipping the
# tag end. Caller is responsible for unique basenames.
image_short = analysis["image"].rsplit("/", 1)[-1]
if len(image_short) > 50:
image_short = image_short[:47] + "..."

# Format cells
unaddr_str = f"✅ {unaddressed}" if unaddressed == 0 else f"🔴 **{unaddressed}**"
Expand Down Expand Up @@ -536,7 +543,10 @@ def print_cli_summary(
images_with_triage = 0
images_with_chainguard = 0

for analysis in cve_analysis:
for analysis in sorted(
cve_analysis,
key=lambda a: a.get("image", "").rsplit("/", 1)[-1].lower(),
):
critical = analysis.get("critical", 0)
high = analysis.get("high", 0)
medium = analysis.get("medium", 0)
Expand Down
20 changes: 17 additions & 3 deletions scanner_py/cli/generate_sbom_report.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,14 @@ def create_generate_sbom_report_parser(subparsers: Any) -> argparse.ArgumentPars
default="sbom-detailed-report.md",
help="Output file path (default: sbom-detailed-report.md)",
)
parser.add_argument(
"--source-label",
help=(
"Logical label for the scan source shown in the report header "
"(overrides the value read from scan-summary.json). Use this when "
"scanning a static image list that is not backed by a K8s namespace."
),
)
parser.add_argument(
"--verbose",
action="store_true",
Expand Down Expand Up @@ -161,6 +169,9 @@ def generate_report(
namespace: str = "pharia-ai",
) -> bool:
"""Generate detailed SBOM report."""
# Sort alphabetically so the per-image sections render in a stable order
# regardless of scan-completion order.
successful_scans = sorted(successful_scans, key=lambda s: s.lower())
total_images = len(successful_scans)

# Calculate overall statistics
Expand Down Expand Up @@ -196,7 +207,7 @@ def generate_report(
# Header
f.write("# Detailed SBOM Analysis Report\n\n")
f.write(f"**Generated:** {datetime.utcnow().strftime('%Y-%m-%dT%H:%M:%SZ')}\n")
f.write(f"**Source:** Successful SBOM scans from {namespace} namespace\n")
f.write(f"**Image source:** `{namespace}`\n")
f.write(f"**Total Images Analyzed:** {total_images}\n\n")
f.write("---\n\n")
f.write("## Executive Summary\n\n")
Expand Down Expand Up @@ -403,8 +414,11 @@ def run_generate_sbom_report(args: argparse.Namespace) -> int:
logger.error("No successful scans found in scan summary")
return 1

# Get namespace
namespace = summary.get("scan_summary", {}).get("namespace", "pharia-ai")
# Resolve label: explicit --source-label wins over scan-summary.json.
namespace = (
getattr(args, "source_label", None)
or summary.get("scan_summary", {}).get("namespace", "pharia-ai")
)

# Generate report
if generate_report(scan_results_dir, output_file, successful_scans, namespace):
Expand Down
61 changes: 49 additions & 12 deletions scanner_py/cli/k8s_scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@

logger = get_logger(__name__)

TRIVY_DB_REPOSITORY = "ghcr.io/aquasecurity/trivy-db:2"


def prepare_trivy_db(verbose: bool = False) -> bool:
"""
Expand All @@ -31,7 +33,7 @@ def prepare_trivy_db(verbose: bool = False) -> bool:

Steps:
1. trivy clean --all (clean existing db)
2. trivy image --download-db-only (download fresh db)
2. trivy image --download-db-only --db-repository ... (download fresh db)

Returns:
True if successful
Expand All @@ -52,9 +54,17 @@ def prepare_trivy_db(verbose: bool = False) -> bool:

# Step 2: Download fresh database
if verbose:
logger.info("Downloading Trivy vulnerability database...")
logger.info(
f"Downloading Trivy vulnerability database from {TRIVY_DB_REPOSITORY}..."
)

download_args = ["trivy", "image", "--download-db-only"]
download_args = [
"trivy",
"image",
"--download-db-only",
"--db-repository",
TRIVY_DB_REPOSITORY,
]
if not verbose:
download_args.append("--quiet")

Expand Down Expand Up @@ -120,6 +130,14 @@ def create_k8s_scanner_parser(subparsers: Any) -> argparse.ArgumentParser:
default="pharia-ai",
help="Kubernetes namespace to scan (default: pharia-ai)",
)
parser.add_argument(
"--source-label",
help=(
"Logical label for the scan source shown in the report header "
"(overrides --namespace / --image-file basename). Use this when "
"scanning a static image list that is not backed by a K8s namespace."
),
)
parser.add_argument(
"--kubeconfig",
help="Path to kubeconfig file",
Expand Down Expand Up @@ -613,7 +631,15 @@ def generate_summary(
failed = [r for r in results if not r.success and not r.skipped]
skipped = [r for r in results if r.skipped]

namespace_label = args.image_file if args.image_file else args.namespace
# Prefer explicit --source-label, else the basename of --image-file, else --namespace.
# Using the full --image-file path as a label produced unreadable "Namespace"
# values like "/home/runner/work/.../images-harbor.txt" in the step summary.
if getattr(args, "source_label", None):
namespace_label = args.source_label
elif args.image_file:
namespace_label = Path(args.image_file).name
else:
namespace_label = args.namespace
summary = ScanSummary(
namespace=namespace_label,
total_images_found=extraction_result.total_found,
Expand Down Expand Up @@ -702,7 +728,7 @@ def generate_markdown_summary(summary: ScanSummary, min_cve_level: str) -> str:
lines.append("")
lines.append(f"| Metric | Value |")
lines.append("|--------|-------|")
lines.append(f"| **Namespace** | `{summary.namespace}` |")
lines.append(f"| **Image source** | `{summary.namespace}` |")
lines.append(f"| **Images Found** | {summary.total_images_found} |")
lines.append(f"| **Images Processed** | {summary.images_processed} |")
lines.append(f"| **Successful Scans** | ✅ {summary.successful_scans} |")
Expand Down Expand Up @@ -742,11 +768,19 @@ def generate_markdown_summary(summary: ScanSummary, min_cve_level: str) -> str:
images_with_triage = 0
images_with_chainguard = 0

for analysis in summary.cve_analysis:
image_short = analysis["image"].split("/")[-1]
# Truncate if too long
if len(image_short) > 40:
image_short = image_short[:37] + "..."
for analysis in sorted(
summary.cve_analysis,
key=lambda a: a.get("image", "").rsplit("/", 1)[-1].lower(),
):
# Show only the image basename (`<image>:<tag>`); strip the registry
# host and project segments. Reports list 30-40 images at a time and
# the project prefix is the same for almost all of them, so it adds
# noise without adding signal. Truncate very long refs (> 50 chars)
# by clipping the tag end. Caller is responsible for ensuring
# basenames are unique across the input image set.
image_short = analysis["image"].rsplit("/", 1)[-1]
if len(image_short) > 50:
image_short = image_short[:47] + "..."

# Get CVE counts
critical = analysis.get("critical", 0)
Expand Down Expand Up @@ -940,8 +974,11 @@ def print_summary(summary: ScanSummary, min_cve_level: str, verbose: bool = Fals
images_with_triage = 0
images_with_chainguard = 0

for analysis in summary.cve_analysis:
image_short = analysis["image"].split("/")[-1][:33]
for analysis in sorted(
summary.cve_analysis,
key=lambda a: a.get("image", "").rsplit("/", 1)[-1].lower(),
):
image_short = analysis["image"].rsplit("/", 1)[-1][:33]

# Get CVE counts
critical = analysis.get("critical", 0)
Expand Down
Loading
Loading