feat(scanner-container): exposed-port surface as new sub-scanner#149
Conversation
Implements the "Container image exposed ports" item from docs/developer/SDK-ROADMAP.md → "Attack Surface Visibility — Port & Service Exposure". Reports what network endpoints a container image declares via Dockerfile EXPOSE — separate from whether those endpoints have known CVEs. "Image exposes 6379/tcp" is a different question from "image has a vulnerable Redis package" and most security reviewers want both. No new scanner module; extends the existing container scanner's sub-scanner orchestration alongside trivy/grype/syft. Default sub-scanner list becomes "trivy,grype,syft,exposure" — opt out by dropping the name from scanners.container.scanners. How it works: - _scan_exposed_ports(image_ref, config) ensures the image is present locally (via container_runtime.pull_image with if-not-present, which is a fast cache hit when trivy/grype/syft already pulled it), runs <runtime> image inspect <ref>, parses Config.ExposedPorts, and emits one Finding per port. - Severity defaults to INFO for ordinary application ports and MEDIUM for ports on the built-in RISKY_PORTS dict: 21/tcp (FTP), 22/tcp (SSH), 23/tcp (Telnet), 25/tcp (SMTP), 110/tcp (POP3), 143/tcp (IMAP), 161/udp (SNMP), 389/tcp (LDAP), 445/tcp (SMB), 3306/tcp (MySQL), 3389/tcp (RDP), 5432/tcp (PostgreSQL), 6379/tcp (Redis), 9200/tcp (Elasticsearch), 11211/tcp (Memcached), 27017/tcp (MongoDB). Each entry cites a "why" in the scanner module docstring (CIS Docker Benchmark §5.8, Shodan unauthorized-database-access reports, CVE-1999-0517 for SNMPv1/v2 community strings, etc.) so future contributors don't tune the list blindly. Config knobs (argus.yml): - scanners.container.expose_warn_ports: list[str] — replaces the built-in WARN list. Empty list demotes every declared port to INFO. - scanners.container.expose_ignore_ports: list[str] — suppress findings entirely. Use for ports the team has explicitly accepted (the app's known 8080/tcp, etc.). Both lists accept "PORT/PROTO" strings; bare "PORT" defaults to tcp; protocol is case-insensitive. Validator rejects malformed entries at config-load time so authoring mistakes surface during argus validate, not at scan time. Finding shape — flows through the existing reporter pipeline (terminal, markdown, sarif, json, github, gitlab, junit), -- severity-threshold filtering, audit trail, and the view-terminal / view-browser UIs without per-reporter custom code: id: EXPOSE-<port>-<proto> severity: INFO or MEDIUM metadata: {port, protocol, common_service, risky, image_ref} Test coverage (29 new tests): - TestParsePortProto: canonical form, bare-port-defaults-tcp, case+whitespace tolerated, 9-case invalid parametrize. - TestScanExposedPorts: single non-risky → INFO, single risky → MEDIUM with service name, multi-port sort+classification, ignore-list suppresses, warn-override replaces defaults, empty-warn-override demotes all to INFO, no exposed ports, no Config block, empty inspect array, no runtime → skipped metadata, pull failure → error metadata, unparseable port logged + skipped (subprocess + container_runtime mocked). - TestExposureSchemaValidation: valid lists accepted, non-list errors, malformed entries error, non-string entries error, "exposure" valid in container sub-scanner list. Out of scope (deferred): - Runtime port enumeration (actually start the container, probe with nmap/ss). Static EXPOSE data is the bulk of the value at a fraction of the operational cost. A runtime variant becomes a separate roadmap item if demand surfaces. Docs + .ai/: - docs/config-reference.md: container scanner description, scanner-specific properties table (new rows for expose_warn_ports and expose_ignore_ports), worked example for attack-surface tuning. - argus.example.yml: commented example showing default sub-scanner set + the two new knobs. - .ai/architecture.yaml: scanners/ description in both SDK blocks updated to mention the four sub-scanners and the RISKY_PORTS watchlist. - docs/developer/SDK-ROADMAP.md: roadmap entry flipped from actionable to shipped with implementation summary. Full suite: 3155 passed (+29 new), 2 skipped.
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
🔒 Argus Container Security ScanBranch: 📊 Combined Findings Summary
Scanned: 4 containers | Build Failures: 0 📦 Container Breakdown
🔍 Detailed Findings by Container🚨 cli - 73 vulnerabilities (33 unique)Image: Combined (Deduplicated)
🔷 Trivy Scanner (73 findings, 33 unique)
...and 23 more ⚓ Grype Scanner (0 findings, 0 unique)✅ No vulnerabilities detected by Grype 🟡 scanner-bandit - 2 vulnerabilities (2 unique)Image: Combined (Deduplicated)
🔷 Trivy Scanner (2 findings, 2 unique)
⚓ Grype Scanner (0 findings, 0 unique)✅ No vulnerabilities detected by Grype
|
| 🚨 Critical | 🟡 Medium | 🔵 Low | Total | Unique | |
|---|---|---|---|---|---|
| 0 | 7 | 44 | 63 | 114 | 50 |
🔷 Trivy Scanner (114 findings, 49 unique)
| CVE | Severity | Package | Version | Fixed |
|---|---|---|---|---|
| CVE-2026-4878 | libcap2 | 1:2.75-10+b8 | N/A | |
| CVE-2025-69720 | libncursesw6 | 6.5+20250216-2 | N/A | |
| CVE-2026-29111 | libsystemd0 | 257.9-1~deb13u1 | N/A | |
| CVE-2025-69720 | libtinfo6 | 6.5+20250216-2 | N/A | |
| CVE-2026-29111 | libudev1 | 257.9-1~deb13u1 | N/A | |
| CVE-2025-69720 | ncurses-base | 6.5+20250216-2 | N/A | |
| CVE-2025-69720 | ncurses-bin | 6.5+20250216-2 | N/A | |
| CVE-2026-27456 | 🟡 MEDIUM | bsdutils | 1:2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | bsdutils | 1:2.41-5 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | libblkid1 | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | libblkid1 | 2.41-5 | N/A |
| CVE-2026-4046 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-4437 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-4438 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-5435 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-5450 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-5928 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-6238 | 🟡 MEDIUM | libc-bin | 2.41-12+deb13u2 | N/A |
| CVE-2026-4046 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-4437 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-4438 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-5435 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-5450 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-5928 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-6238 | 🟡 MEDIUM | libc6 | 2.41-12+deb13u2 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | liblastlog2-2 | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | liblastlog2-2 | 2.41-5 | N/A |
| CVE-2026-34743 | 🟡 MEDIUM | liblzma5 | 5.8.1-1 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | libmount1 | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | libmount1 | 2.41-5 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | libsmartcols1 | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | libsmartcols1 | 2.41-5 | N/A |
| CVE-2026-40225 | 🟡 MEDIUM | libsystemd0 | 257.9-1~deb13u1 | N/A |
| CVE-2026-40226 | 🟡 MEDIUM | libsystemd0 | 257.9-1~deb13u1 | N/A |
| CVE-2026-4105 | 🟡 MEDIUM | libsystemd0 | 257.9-1~deb13u1 | N/A |
| CVE-2026-40225 | 🟡 MEDIUM | libudev1 | 257.9-1~deb13u1 | N/A |
| CVE-2026-40226 | 🟡 MEDIUM | libudev1 | 257.9-1~deb13u1 | N/A |
| CVE-2026-4105 | 🟡 MEDIUM | libudev1 | 257.9-1~deb13u1 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | libuuid1 | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | libuuid1 | 2.41-5 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | login | 1:4.16.0-2+really2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | login | 1:4.16.0-2+really2.41-5 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | mount | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | mount | 2.41-5 | N/A |
| CVE-2026-5958 | 🟡 MEDIUM | sed | 4.9-2 | N/A |
| CVE-2026-5704 | 🟡 MEDIUM | tar | 1.35+dfsg-3.1 | N/A |
| CVE-2026-27456 | 🟡 MEDIUM | util-linux | 2.41-5 | N/A |
| CVE-2026-3184 | 🟡 MEDIUM | util-linux | 2.41-5 | N/A |
| CVE-2026-27171 | 🟡 MEDIUM | zlib1g | 1:1.3.dfsg+really1.3.1-1+b1 | N/A |
| CVE-2026-3219 | 🟡 MEDIUM | pip | 26.0.1 | N/A |
...and 64 more
⚓ Grype Scanner (0 findings, 0 unique)
✅ No vulnerabilities detected by Grype
⚠️ scanner-supply-chain - 17 vulnerabilities (17 unique)
Image: ghcr.io/huntridge-labs/argus/scanner-supply-chain:f049ad2288fb67e72e3ace632c7165e55624fd02
Combined (Deduplicated)
| 🚨 Critical | 🟡 Medium | 🔵 Low | Total | Unique | |
|---|---|---|---|---|---|
| 0 | 9 | 8 | 0 | 17 | 17 |
🔷 Trivy Scanner (17 findings, 17 unique)
| CVE | Severity | Package | Version | Fixed |
|---|---|---|---|---|
| CVE-2026-32280 | stdlib | v1.26.1 | 1.25.9, 1.26.2 | |
| CVE-2026-32281 | stdlib | v1.26.1 | 1.25.9, 1.26.2 | |
| CVE-2026-32283 | stdlib | v1.26.1 | 1.25.9, 1.26.2 | |
| CVE-2026-33810 | stdlib | v1.26.1 | 1.26.2 | |
| CVE-2026-33811 | stdlib | v1.26.1 | 1.25.10, 1.26.3 | |
| CVE-2026-33814 | stdlib | v1.26.1 | 1.25.10, 1.26.3 | |
| CVE-2026-39820 | stdlib | v1.26.1 | 1.25.10, 1.26.3 | |
| CVE-2026-39836 | stdlib | v1.26.1 | 1.25.10, 1.26.3 | |
| CVE-2026-42499 | stdlib | v1.26.1 | 1.25.10, 1.26.3 | |
| CVE-2026-3219 | 🟡 MEDIUM | pip | 26.0.1 | N/A |
| CVE-2026-6357 | 🟡 MEDIUM | pip | 26.0.1 | 26.1 |
| CVE-2026-32282 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.9, 1.26.2 |
| CVE-2026-32288 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.9, 1.26.2 |
| CVE-2026-32289 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.9, 1.26.2 |
| CVE-2026-39823 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.10, 1.26.3 |
| CVE-2026-39825 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.10, 1.26.3 |
| CVE-2026-39826 | 🟡 MEDIUM | stdlib | v1.26.1 | 1.25.10, 1.26.3 |
⚓ Grype Scanner (0 findings, 0 unique)
✅ No vulnerabilities detected by Grype
Generated by Argus
Description
Implements the Container image exposed ports item from
docs/developer/SDK-ROADMAP.md→ "Attack Surface Visibility — Port & Service Exposure". Reports what network endpoints a container image declares via DockerfileEXPOSE— separate from whether those endpoints have known CVEs. "Image exposes 6379/tcp" is a different question from "image has a vulnerable Redis package" and most security reviewers want both.Changes Made
exposuresub-scanner inside the existing container scannerDetails
No new scanner module — extends
argus/scanners/container.py's sub-scanner orchestration alongside trivy/grype/syft. The data is free:<runtime> image inspect <ref>returnsConfig.ExposedPortsand the container scanner already pulls every image it scans, so the inspect is a fast cache hit.Default sub-scanner set becomes
"trivy,grype,syft,exposure". Opt out by droppingexposurefromscanners.container.scanners.Output shape — one
Findingper declared port:Findings flow through the existing reporter pipeline (terminal, markdown, sarif, json, github, gitlab, junit),
--severity-thresholdfiltering, audit trail, and theview-terminal/view-browserUIs without per-reporter custom code.Built-in
RISKY_PORTSwatchlist (MEDIUM severity by default):public) — CVE-1999-0517Each entry cites a "why" in the scanner docstring so future contributors don't tune the list blindly. Adding a new entry requires the same.
Config knobs (in
argus.yml):Both lists accept
"PORT/PROTO"strings (bare"PORT"defaults to tcp; protocol case-insensitive). Schema validator errors on malformed entries at config-load time.Testing
Test Results
29 new tests:
TestParsePortProtoTestScanExposedPortsTestExposureSchemaValidationexposurevalid in container sub-scanner listFull suite: 3155 passed (+29 new), 2 skipped, 7 deselected.
Security Considerations
Security Details
Closes a real attack-surface visibility gap. Today argus reports vulnerabilities (Trivy/Grype find CVEs in installed packages) and SBOM components (Syft) but doesn't surface what network endpoints an image declares. A reviewer auditing a base image for the first time wants to know "does this expose anything I didn't expect?" — the new sub-scanner answers that without requiring users to read
docker inspectJSON by hand.The classification is conservative: only well-known risky-defaults get MEDIUM (the watchlist is small and cited); ordinary application ports stay INFO. False-positive rate is low because the data is the image's own declared intent.
AI Context Updates (.ai/)
.ai/architecture.yamlupdated —scanners/description in both SDK blocks updated to mention the four sub-scanners and theRISKY_PORTSwatchlist..ai/workflows.yamlupdated.ai/decisions.yamlupdated — implementation of an already-decided roadmap item; no new ADR..ai/errors.yamlupdatedChecklist
Related Issues
Closes the Container image exposed ports item in
docs/developer/SDK-ROADMAP.md→ "Attack Surface Visibility — Port & Service Exposure". The companion OS-image research item remains queued.Out of scope (deferred): runtime port enumeration (actually start the container, probe with
nmap/ss). StaticEXPOSEdata is the bulk of the value at a fraction of the operational cost. A runtime variant becomes a separate roadmap item if consumer demand surfaces.Screenshots/Logs (if applicable)
Diff: 7 files, +638 / -88.