Stella-vs-Trivy benchmark enhancement — consolidated findings
Status: research capture (pre-plan). Date: 2026-06-21. Purpose: single durable reference consolidating four investigations, so the implementation sprint is built on complete knowledge. No code changed yet. Source workflows (full transcripts under the session subagents dir): gap analysis
w99iukh19, scoring reconw9km5w72n, oracle-data acquisitionw38ebc66u. Working dir of the comparer:tools/benchmarks/stella-vs-trivy/.
1. Current state of the comparer
The lane builds 500 real OSS projects into OCI images and scans each with three scanners — Stella, Trivy (Aqua), Grype (Anchore). It measures only:
- PURL counts per tool (arrays + counts +
purlDeltaPct) —compare-project.shSBOM block. - Alias-aware CVE-ID overlap, three pairwise sets (Stella↔Trivy, Stella↔Grype, Trivy↔Grype) —
compare-project.sh:402-510. - Stella-only reachability estimate — regex binning of a
reachabilitystring,compare-project.sh:436-439. - Trivy source-tree FP annotation —
annotate-trivy-fixture-fps.py(one-directional).
There is no ground truth — every metric is tool-vs-tool. The harness’s own analysis-problem-and-winning-groups.md documents a prior wrong “Trivy wins” conclusion that was a methodology confound (deployed-vs-all scope). This is the core motivation: tool-vs-tool counting is not just incomplete, it has actively misled.
2. The 10 evaluation dimensions vs current coverage
| # | Dimension | Current coverage |
|---|---|---|
| 1 | Package-discovery depth | Partial — single PURL scalar, no by-type, no truth |
| 2 | True-positive recall | None — tool-vs-tool only |
| 3 | False-positive control | Partial — Trivy-fixture FP only, one-directional |
| 4 | Evidence quality | None — counts only |
| 5 | Reachability / exploitability | Partial — Stella-only heuristic; no KEV/EPSS surfaced |
| 6 | SBOM compatibility | None — never feeds foreign SBOMs to Stella |
| 7 | Offline replay / determinism | None in the bench |
| 8 | Remediation quality | None measured (data present) |
| 9 | Speed / footprint / air-gap | None captured |
| 10 | Policy / audit story | None |
3. Locked decisions
- Tools to add: osv-scanner (independent OSV lineage), Syft (SBOM-interop producer). Clair dropped (Postgres cost; marginal RHEL-only value).
- Lineages for consensus: Stella (Concelier) · Trivy (Aqua) · Grype (Anchore) · osv-scanner (OSV). Grype+osv-scanner partly correlate on OSV → consensus is a fallback proxy only.
- Oracle = rule-based, no hand-labeling. Truth derived from advisory version-ranges + N-of-M agreement + distro backport rules.
- Label precedence on conflict: distro tracker wins for OS packages (deb/apk/rpm); advisory-range wins for language packages.
- Oracle data = LOCAL Concelier Postgres
stellaops_platform.vuln.advisory_affected(richversion_rangeJSONB +versions_fixed[]+database_specific, independent of the compact DB the matcher reads). No internet fetch — confirmed present locally and measured (§10). Benchmark projects are latest-clone + cached (already the harness model). The fetch-not-vendor /oracle-sources.lock.jsonapproach (§7–9) is retained only as a fallback for environments without a populated Concelier; if ever used, data stays in a git-ignored cache with only URLs+digests committed (removes the redistribution trigger incl. RHEL — no counsel gate). - Comparer is developer-only: extend the existing lane scripts (not a parallel system); the scorer is a C# dev-only tool built/run by the lane, referencing product libs but shipped in no release artifact.
- Gating: report-and-trend first; add CI fail-gates once oracle coverage/label-stability is proven.
- VEX: annotate by default; explicit flag to suppress. Capsule signing: local key, not Sigstore/Rekor.
4. Reporter / product gaps (Stella sbom check) — 14
Recurring theme: Stella computes/stores the signal but the offline read path projects a thin finding.
| ID | Gap | Key evidence | Effort |
|---|---|---|---|
| RPT-01 | CVSS-vector not emitted (present in adv at finding time) | SbomCheckCommand.cs:415-442; reader CompactVulnDbReader.cs:140-147; exporter writes it VulnDbCommandGroup.cs:294,354 | trivial |
| RPT-02 | EPSS + CWE absent; exporter hard-codes them null | VulnDbCommandGroup.cs:354; schema cols exist CompactVulnDbSchema.cs:31-33; Postgres advisory_weaknesses 001_v1...sql:322-330 | small (re-export) |
| RPT-03 | Reachability verdict can’t distinguish not-run vs no-evidence vs not-observed | SbomCheckCommand.cs:229-259, RenderJson :595-596; sinks Go/Rust only VulnDbCommandGroup.cs:445-541 | small |
| RPT-04 | Remediation: no fixedKind / minimal-upgrade; ranges mixed with versions | SbomCheckCommand.cs:435; LocalAdvisoryMatcher.cs:289-301; CompactVulnDbWriter.cs:140-141 | medium |
| EVID-01 | File-path/location dropped by the CLI’s lighter SbomParser | Scanner emits CycloneDxComposer.cs:360,378,466; parser has none ISbomParser.cs:96-123, SbomParser.cs:195-306 | medium |
| EVID-02 | Layer-digest dropped | Scanner emits CycloneDxComposer.cs:432, LanguageComponentMapper.cs:108; ignored SbomParser.cs:332-354 | small |
| EVID-03 | Advisory-source/feed provenance not exported | Postgres source_id→sources.key 001_v1...sql:202-216; exporter no join VulnDbCommandGroup.cs:250-266 | medium (v3) |
| EVID-04 | CPE parsed then discarded | SbomParser.cs:222-248,556-587, ISbomParser.cs:101-102; dropped SbomCheckCommand.cs:211-214 | small |
| EVID-05 | No per-advisory match confidence | LocalAdvisoryMatcher.cs:29-35,248-266,350-352 (signals computed, discarded) | medium |
| KEV-01 | KEV connector + vuln.kev_flags exist but never exported/surfaced | Connector.Kev/KevConnector.cs:388-400; table 001_v1...sql:336-353; absent from CompactVulnDbSchema.cs | medium (v3) |
| DET-01 | RFC-8785 canonical-id computed in Scanner, never stamped into sbom check output | canonical-sbom-id-v1.md:9,92-97; RenderJson SbomCheckCommand.cs:546-617 | medium |
| DET-02 | No asserted byte-stability test | SbomCheckReachabilityTests.cs, SbomCheckScopeTests.cs (neither asserts it) | small |
| AUDIT-01 | sbom check never applies VEX (table + reader exist) | vex table CompactVulnDbSchema.cs:45-52, reader CompactVulnDbReader.cs:151-175 | medium |
| AUDIT-02 | No signed/replayable offline decision capsule from sbom check | SignCommandGroup.cs (mock audit :467-472), ReplayCommandGroup, plain JSON SbomCheckCommand.cs:276-284 | large |
v3 compact-DB bump batches EVID-03 + KEV-01 (new columns); RPT-02 rides the same re-export (exporter fix only). One re-export + mirror refresh covers all three.
Product open questions: EPSS source table for the join; EVID-05 confidence rubric; AUDIT-01 VEX default; AUDIT-02 capsule signing key; EVID-04 CPE synthesis policy.
5. Harness gaps — 17
| ID | Gap | Effort | Notes |
|---|---|---|---|
| ORACLE-01 | Advisory-range labels from independent OSV data via reused EcosystemVersionFilter | large | the core; non-circular (raw OSV ≠ Stella DB) |
| ORACLE-02 | N-of-M consensus across independent lineages | medium | fallback proxy when no advisory range |
| ORACLE-03 | Backport/FP truth from distro trackers | large | distro-wins-OS precedence |
| ORACLE-04 | 5-tier label schema + per-tier precision/recall/F1 (unknown excluded → coverage%) | medium | reuse stella bench framing |
| WIRE-01 | Determinism double-run, byte-identical findings | small | normalize then sha256 |
| WIRE-02 | SBOM interop: feed Syft/Trivy SBOMs to stella sbom check; schema-validate exports | medium | ingest confirmed working |
| WIRE-03 | Speed / peak-RSS / DB-size / --network none assertion | medium | timings report-only, not gated |
| WIRE-04 | Remediation %: share of findings with actionable fix | trivial | jq over existing artifacts |
| WIRE-05 | Discovery depth by type (os/lang/static-binary) | small | prepare.json already has purlsByEco |
| TOOL-01 | osv-scanner column + consensus lineage | medium | resolve-osv-scanner.sh |
| TOOL-02 | — | dropped | |
| TOOL-03 | Syft SBOM producer (feeds WIRE-02) | small | resolve-syft.sh, no DB |
| METH-01 | Discovery-truth from lockfiles/distro manifests (rule-based) | large | denominator for discovery P/R |
| METH-02 | Symmetric FP classification (Stella-only, Grype-only too) | medium | generalize the annotator |
| METH-03 | VEX-suppression behavioral test via stella vex apply | medium | Tier-2d correctness test |
| METH-04 | Evidence-quality scorer over each tool | small | where Stella richness shows |
| METH-05 | Signed decision-capsule for the bench’s own evidence | medium | reuse stella attest |
5a. Reachability dimension — “does the code reach the vulnerable call” (REACH-01…08)
Scope: genuine call-path reachability (the app’s code graph reaching the vulnerable function/sink), not scoring a metric. No competitor emits it (Trivy/Grype/osv-scanner) → Stella-only axis whose value is noise reduction: “of N found vulns, M are reachable-proven.” Source workflow wpbgy1si3.
Headline defect: the lane never runs the engine. compare-project.sh:292 calls sbom check with no --reachability/--source, so findings.reachability is always null and the regex estimate (:436-439) bins null into a permanent zero. The current reachability number is dead.
| ID | Gap / action | Effort |
|---|---|---|
| REACH-01 | Wire --reachability --source <src> --reach-lang go|rust into the stella call for eligible cohorts | small |
| REACH-02 | Retain the FULL Go/Rust source tree (prepare uses sparse/deletes it; the SSA extractor needs the module tree) | medium |
| REACH-03 | Pre-flight the external SSA tool (stella-callgraph-go/Rust); fail loud, never pass off the static fallback as the real engine | small |
| REACH-04 | Replace the regex heuristic with the real wire verdict + reason + witnessCount (schema v3→v4) | small |
| REACH-05 | 3-state status (engine-not-run / assessed / error); never emit fake not-observed for the 13 non-Go/Rust cohorts | small |
| REACH-06 | Cohort-level noise-reduction funnel in the launcher aggregate + report | medium |
| REACH-07 | Cost control: reachability is an opt-in Go/Rust shard (~60 of ~340 projects) with per-project timeout | medium |
| REACH-08 | Report sink coverage so unknown(no-sinks) is distinct from real not-observed | small |
Reporting model: funnel found → assessed → reachable (proven+likely) → proven, gated on reachabilityEngine.ran; headline reachableProvenRate = proven/assessed, noiseReductionRate = notObserved/assessed. Competitor columns stay N/A by design (that is the story). Coverage honestly bounded to Go+Rust today (9,077 sinks); all other cohorts shown as “reachability: not measured (no sinks)”.
Ground truth (no hand-labeling): verdict is self-certifying — proven requires a non-empty witness path; unknown(no-sinks) is rule-derived. Correctness/regression stays on the curated tier corpus (ReachabilityTierMetricHarness, promoted out of test asm — the same metrics engine ORACLE-04 reuses) + a precision-only “unreachable-by-rule” cross-check (advisory sink symbol provably absent from the call graph). Exploitability funnel: found → reachable:proven → KEV-listed → high-EPSS (reads KEV-01/RPT-02 fields).
6. Scoring reuse map (the big finding: ORACLE-04 is mostly assembly)
Substantial scoring already exists — and the highest-value pieces are all C#. ORACLE-04 becomes assembly of tested code + one new rule engine.
| Need | Reuse this | Location | Level |
|---|---|---|---|
| Per-tier TP/FP/FN→P/R/F1 over a key-union | ReachabilityTierMetricHarness.Compute | Scanner.Reachability.Tests/.../ReachabilityTierCorpusTests.cs:358 | high (promote out of test asm; swap key→(image,pkg,CVE), enum→truth tiers) |
| Classify-then-count + Unknown bucket; P/R/F1 formulas; by-dimension dict | MetricsCalculator / BenchmarkMetrics (orphaned singular Scanner.Benchmark) | src/Scanner/__Libraries/StellaOps.Scanner.Benchmark/Metrics/*.cs:25,95,31-52,115-131 | high (revive + add tests; 0 csproj refs today) |
Trivy/Grype/Syft → NormalizedFinding | singular Scanner.Benchmark adapters | .../Harness/{Trivy,Grype,Syft}Adapter.cs | high (add osv-scanner + Stella adapters) |
| Finding ingestion (SARIF+JSON) + set-arith (overlap/Jaccard) | VendorParityAnalyzer | Tools/StellaOps.Bench/.../VendorParityAnalyzer.cs:9,197 | high (truth-side ⇒ overlap=TP, truth-only=FN, scanner-only=FP) |
| Wired regression gate (warn/fail bands, delta, exit code, md/json report) | KpiRegressionService + groundtruth baseline check | BinaryIndex.GroundTruth.Reproducible/Services/KpiRegressionService.cs:89,310 | high (DI-registered, CLI exposes thresholds) |
| Consensus label + confidence (weighted vote, margin, unanimity) | VexConsensusEngine (+ TrustWeightEngine) | VexLens.Core/Consensus/VexConsensusEngine.cs:88 | high (ORACLE-02; generalize VexStatus→truth tier) |
| Rule-weighted confidence shape + labeled-corpus accuracy gate template | LinksetCorrelationV2 + RealWorldLinksetAccuracyCorpusTests/AccuracyMatrix | Concelier.Core/Linksets/LinksetCorrelationV2.cs:98 | high |
| Alias-aware (image,CVE) predictions = scorer INPUT | live compare-project.sh jq | tools/benchmarks/stella-vs-trivy/scripts/compare-project.sh:402-460 | high (reuse the alias normalization) |
| Rule-based FP labels (fixture/placeholder) seed | annotate-trivy-fixture-fps.py | tools/benchmarks/.../annotate-trivy-fixture-fps.py:90,142 | medium |
Do NOT reuse for evaluation: Policy EWS / AdvancedScoringEngine and Findings RiskEngine — those are risk prioritization (0-100 danger), orthogonal to correctness. Do NOT extend the doc’s stella bench run plural-Benchmarks path — it is unwired (not in CommandFactory; ICorpusRunner has no prod impl).
Genuinely net-new: the 5-tier TruthTier enum + the rule engine assigning labels per (image,pkg,CVE) (ORACLE-01/02/03 — nothing in the repo does (image,pkg,CVE) truth-labeling); the composite key; an osv-scanner + Stella-native adapter; promote the tier-metric harness; revive the orphaned Benchmark lib.
7. Oracle data — language/library lineages
Decision (2026-06-21): the oracle reads these sources from the local Concelier Postgres (
vuln.advisory_affected), NOT by re-fetching. The URLs/formats below remain the authoritative map and the fallback for un-populated environments. Measured local coverage is in §10.
Authoritative URL/format map is already in the repo: every per-ecosystem language connector is a no-op alias of the single real OSV fetcher.
| ID | Source | Upstream | Coverage / notes | License |
|---|---|---|---|---|
| osv-bulk | OSV.dev per-ecosystem all.zip | osv-vulnerabilities.storage.googleapis.com/<Eco>/all.zip — OsvOptions.cs:9,21,42, OsvConnector.cs:705,736,1054 | ~11/15 cohorts (npm-family, PyPI, Maven, Go, crates, Packagist, RubyGems, Hex, Pub). npm zip ~207 MB (OsvConnector.cs:113); set ~400-600 MB est. | CC-BY-4.0 |
| ghsa | GitHub Security Advisories | api.github.com/advisories — GhsaOptions.cs:9,25,33 | Closes dotnet + swift (absent from OSV buckets) → ~13/15. Needs a GitHub token. Enrichment/cross-check (also feeds OSV upstream). | CC-BY-4.0 |
| nvd | NVD CVE API 2.0 | services.nvd.nist.gov/rest/json/cves/2.0 | CVE-level cross-walk only (CPE, not PURL) — tie-breaker, 0 version-range cohorts. | US-gov public domain |
| osv-scanner-offline-db | osv-scanner bundle | same OSV upstream | Do NOT use as the oracle — circular (osv-scanner is a scored tool). Fetch raw all.zip instead. | Apache-2.0 / CC-BY-4.0 |
Range-evaluator independence (recommended): reuse EcosystemVersionFilter.IsStillVulnerable (EcosystemVersionFilter.cs:36) fed raw upstream data — shares the algorithm, not the data, keeping it non-circular. A neutral re-implementation is only warranted to audit the comparator itself (then run both and diff).
8. Oracle data — distro lineages
Cohort distro exposure comes from the runtime base images the lane builds.
| ID | Source | Upstream | Coverage / notes |
|---|---|---|---|
| debian-tracker | Debian Security Tracker JSON | security-tracker.debian.org/tracker/data/json — DebianOptions.cs:71; fixed-in semantics DebianTrackerJsonParser.cs:29-58 | dotnet(aspnet)/python-slim/swift-slim/dart bases (bookworm/bullseye/trixie). ~75 MB. |
| ubuntu | USN JSON + CSAF/OVAL | ubuntu.com/security/notices.json UbuntuOptions.cs:13; security-metadata.canonical.com/vex/vex-all.tar.xz UbuntuConnectorOptions.cs:17 | java cohort (eclipse-temurin jammy). Pick one canonical fixed-in source. |
| alpine-secdb | Alpine secdb | secdb.alpinelinux.org/<rel>/<repo>.json — AlpineOptions.cs:13,30,35, AlpineConnector.cs:363-366 | Highest — ~8-9 cohorts (cpp/go/rust/php/ruby/elixir/bun/deno + node:lts-alpine). ~20-60 MB. |
| redhat-hydra | Red Hat CSAF (OVAL v2 alt) | access.redhat.com/hydra/rest/securitydata — RedHatOptions.cs:13,18; anon access RedHatRuntimeOverlay.cs:118 | ~0-5% of current cohort. Keep lazy (fetch only if an rpm PURL appears). |
| suse-csaf | SUSE/openSUSE CSAF | ftp.suse.com/pub/projects/security/csaf/ — SuseOptions.cs:12,17 | ~0% current. Lazy, future expansion only. |
License-by-reference confirmed clean for all (incl. RHEL — fetch-by-reference, no committed data = consumer, not redistributor). Document the snapshot date per AGENTS.md §2.6.
9. Integration into the existing lane
- New
scripts/resolve-oracle-data.shmodeled byte-for-byte onresolve-grype.sh(same--cache-dir/--metadata/--envflags). Reads the lock, GETs each pinned artifact, verifies sha256 (fail-closed on drift), writesoracle.env+oracle-metadata.json(stellaops.bench.oracle/v1). - Cache:
$BENCH_CACHE_DIR/oracle/— sibling totrivy/andgrype/(project-runner.sh:556,560), already outside the repo on CI; add a.gitignorerule for the localtmp/**/oracleper §2.9. - Launch wiring: add
--resolve-oracle-data/--no-...flags + a resolve block after the Grype block inlaunch-trivy-vs-stella-ops.sh(mirror lines ~24-25,88-91,660-672); add to thebash -nsyntax-check (~436-437) and the script-copy list (project-runner.sh:71-73). - Workflow: add a “Resolve oracle data” step before “Run benchmark” in
.gitea/workflows/stella-vs-trivy-benchmark.yml(~after line 140); optional cache key on the lock digests. - C# dev tool (e.g.
src/Tools/Bench/StellaOps.Bench.Oracle, built/run by the lane, shipped in no release): reads eachsbom.cdx.json+ORACLE_DATA_DIR, derives distro+release from OS-package PURLs, reuses connector parser semantics (DebianTrackerJsonParser,AlpineSecDbDto, RedHat/SUSE CSAF shapes) +EcosystemVersionFilterfor ranges, emitsoracle.jsonnext tocompare.json. - Release detection: OS-package PURLs in
sbom.cdx.json(parsed from/etc/os-releaseby the OS analyzer) key each image to the right tracker branch — confirm PURLs are release-qualified (pkg:deb/debian-12/...vs barepkg:deb/debian/...).
oracle-sources.lock.json (committed — URLs + digests + versions ONLY)
{
"schema": "stellaops.bench.oracleSources/v1",
"generatedAt": "2026-06-21T00:00:00Z",
"sources": [
{ "id": "debian-tracker-json", "distro": "debian", "releases": ["bookworm","bullseye","trixie"],
"format": "json", "url": "https://security-tracker.debian.org/tracker/data/json",
"sha256": "<hex>", "version": "snapshot-2026-06-21", "approxBytes": 78643200,
"cachePath": "oracle/debian/data.json" },
{ "id": "alpine-secdb-v3.20-main", "distro": "alpine", "releases": ["v3.20"], "repo": "main",
"format": "secdb", "url": "https://secdb.alpinelinux.org/v3.20/main.json",
"sha256": "<hex>", "version": "v3.20", "cachePath": "oracle/alpine/v3.20/main.json" },
{ "id": "osv-npm", "ecosystem": "npm", "format": "osv-zip",
"url": "https://osv-vulnerabilities.storage.googleapis.com/npm/all.zip",
"sha256": "<hex>", "etag": "<etag>", "approxBytes": 217000000, "cachePath": "oracle/osv/npm/all.zip" }
// redhat/suse marked "lazy": true (fetch only when an rpm PURL appears)
]
}
10. Coverage summary
Measured from the local store (stellaops_platform.vuln.advisory_affected, 2026-06-21) — distinct advisories per ecosystem:
| Cohort(s) | Local ecosystem | Distinct advisories |
|---|---|---|
| js/ts/bun/deno | npm | 178,406 |
| python · go · java · php | pypi · golang · maven · composer | 12,932 · 7,251 · 6,698 · 6,247 |
| rust · ruby · dotnet | cargo · gem · nuget | 2,505 · 2,055 · 1,832 |
| elixir · swift · dart | hex · swift · pub | 99 · 18 · 13 (thin but present) |
| cpp | (no conan; generic/cpe only) | real gap → oracle-low-confidence |
| distros | deb · rpm · apk | 57,787 · 1,768 · 8,848 |
So NuGet & Swift ARE present locally (the OSV-bucket concern is moot — GHSA backfill is already ingested); dart/elixir/swift are thin-but-present; cpp is the only true gap; distros deb/rpm/apk are strong. All from local data, zero fetch. Estimates below are superseded by the measured table above.
- Libraries (estimates, superseded): OSV directly ~11/15; +GHSA → ~13/15 (~87%). Hard gap: cpp (no clean PURL-ranged ecosystem). dotnet/swift depend on GHSA enrichment, not a dedicated OSV bucket.
- Distros: Alpine + Debian + Ubuntu cover ≈100% of the crafted base images (where OS backport FPs arise). RHEL/SUSE ≈0% of the current cohort (lazy, future).
- Scoring: P/R/F1 math, ingestion, regression gate, consensus all exist (C#); net-new = the (image,pkg,CVE) truth-label rule engine.
11. Open questions / to-confirm before/while planning
Data realities (only npm’s ~207 MB is verified in-repo; rest are estimates):
- Actual on-disk sizes of non-npm OSV zips, Debian
data.json, Ubuntuvex-all.tar.xz, Alpine per-repo. - Do NuGet (dotnet) and Swift have working OSV
all.zipbuckets now? (OSV connector deliberately omits them — in-repo signal they were unreliable.) - cpp authoritative truth source — likely mark cpp findings oracle-low-confidence.
- Ubuntu canonical fixed-in source: USN JSON vs CSAF
vex-allvs per-release OVAL. - Are bench SBOM OS-package PURLs release-qualified (so branch selection is unambiguous)?
Provisioning / determinism / posture: 6. GitHub token (GHSA) + NVD API key — where do they come from in the air-gapped/CI lane? 7. Snapshot-freeze: trackers update continuously → lock pins a snapshot, resolve fails-closed on drift; lock refreshed deliberately, not per run. 8. Re-confirm RHEL/SUSE fetch-by-reference posture + record snapshot date (§2.6/§2.8).
Scorer: 9. Granularity (image,pkg,CVE) — confirm every scanner carries package identity at compare time (Trivy PkgName / Grype artifact.name / Stella component). 10. Which (scanner,tier) the regression gate fires on; is coverage% gated or report-only.
12. Sources
- Gap analysis — workflow
w99iukh19(14 reporter + 17 harness gaps). - Scoring recon — workflow
w9km5w72n(9 bench-CLI + 10 repo-wide scoring assets). - Oracle-data acquisition — workflow
w38ebc66u(4 library + 5 distro sources). - Harness baseline —
tools/benchmarks/stella-vs-trivy/{README.md,ci-lane.md,scripts/compare-project.sh}; sprintdocs/implplan/SPRINT_20260618_001_Tools_stella_vs_trivy_expanded_benchmark_ci.md.
