Security Policy
How argus handles user secrets, container image provenance, and the
threat model it defends against. Companion to
docs/config-reference.md — where credentials
go in config.
TL;DR
| Concern | What argus does |
|---|---|
Credentials in argus.yml |
Three forms accepted (literal / <field>_env / CLI stdin). Validator warns at config-load if a literal looks like a vendor secret (gh*, AKIA, AIza, etc.). |
Credential values on docker run argv |
Never. The engine passes -e NAME (no value); values live in the python subprocess's private env, inherited by docker by name. |
| Credential values in logs / audit / findings | Never. secrets.py logs field names only. Finding.__post_init__ redacts vendor-prefix tokens (ADR-022). Audit trail captures structured metadata, not env dicts or command lines. |
| Container image provenance | Argus-owned images are cosign-verified (Sigstore keyless). Third-party images with @sha256: digest pins are verified by Docker's pull-time content-hash match. Tag-only third-party images surface a single WARNING per scan. |
| Scanner output that contains literal secrets (e.g. gitleaks match field) | Per-scanner redaction at parse time + a pattern-based safety net at Finding construction (ADR-022). |
If you're configuring argus, the practical advice is one line: use
<field>_env: ENV_VAR_NAME for credentials, set the env var in the
runner's environment, never paste literals into VCS-tracked YAML.
Threat model
What argus defends against
- Local users on the scan host scraping argv.
ps -ef,/proc/<pid>/cmdline,docker inspect— all of these expose process argv to any local user. Argus passes credentials by name only (-e NAME); values live in a subprocess env dict accessible only to the python process and its docker child. - VCS-tracked YAML leaking credentials. The validator warns on
vendor-shaped literals at config-load time. The
<field>_envpattern keeps credential values out of the file entirely. - Log files / audit trail leaking credentials.
argus.core.secretsnever logs values.argus/audit/logger.pyandargus/audit/manifest.pydo not capture command lines or env dicts. The engine's debug log of the docker invocation joinscontainer_args(post-image scanner argv) — the-e NAMEflags live indocker_cmdbefore the image and are not part of that log. - Scanner output echoing literal secrets into findings. The
redact module (
argus/core/redact.py) runs atFinding.__post_init__, scrubbing vendor-prefix tokens fromtitle,description, and every string inmetadatabefore downstream consumers see them. Per-scanner first-pass redaction sits upstream of this safety net for known leak fields (gitleaks'sMatch, bandit's B105/106/107 hardcoded-credentialissue_text, etc.). - Supply-chain attacks via tampered argus-owned images. The 4
container images argus publishes (
scanner-bandit,scanner-opengrep,scanner-supply-chain,cli) are cosign-signed at release with keyless Sigstore. Argus verifies each pull against the publishing workflow's certificate identity and the GitHub Actions OIDC issuer. Verification failure aborts the scanner. - Tracebacks / crash dumps leaking values. Env vars are set at process-spawn time, not on the python call stack — exceptions raised inside scanners don't capture them in tracebacks.
What argus does NOT defend against
These are out of scope. If your threat model includes them, argus is one of several layers and you'll need additional controls.
- Root or same-uid attacker on the scan host. A user with
/proc/<pid>/environread access can extract the subprocess env directly. This is a strictly tighter access set than whatpsgives the average local user, but not zero — root, or another process running as the same uid, can still see the env. - Compromised scanner image. If
aquasec/trivy@sha256:...is itself malicious (e.g., upstream account compromise), argus hands it the credentials we resolved for registry auth. The container has full read on/workspaceand any env vars we passed. We pin third-party scanner versions via Dependabot/Renovate but cannot independently verify upstream image attestations — that's an upstream trust problem. - Malicious YAML literal that doesn't match a vendor prefix.
The validator's "looks like a literal secret" heuristic catches
vendor-prefixed tokens (
gh*,AKIA*,AIza*,glpat-*,sk_live_*,npm_*,xox*-). It does not catch generic high-entropy strings, basic-auth passwords likehunter2, or custom-format internal tokens. Those still work via the back-compat literal path but with no warning — gitleaks's domain, not ours. - Network-level interception of credentials. Argus passes credentials to scanners which may transmit them to registries over HTTPS. We don't pin TLS certs or enforce mTLS at the argus layer; that's the underlying tool's responsibility.
- Memory scraping after credential resolution. Python strings are immutable and remain in memory until GC. Argus does not zero credential strings explicitly — this is a fundamental Python limitation, not unique to argus.
- Renovate/Dependabot account compromise. Image updates flow through these automation paths. Verification on pull catches tampering of argus-owned images. Third-party image updates rely on Renovate's signature verification (where available) and digest pinning (where we've migrated to it).
Where credentials can live — precedence
Highest precedence first; the first available path wins.
1. CLI stdin (--*-password-stdin)
echo "$REGISTRY_TOKEN" | argus scan --registry-password-stdin --config argus.yml
echo "$APP_PASSWORD" | argus scan zap --zap-auth-password-stdin --target https://app
Mirrors docker login --password-stdin. The value:
- Is read once from stdin (a single trailing newline is stripped; multi-line tokens like PEM contents are preserved exactly).
- Lives in a process-local slot registry in
argus/core/secrets.py; scanners pull it at scan time viaget_stdin_override(slot). - Never reaches the per-scanner config dict, so it cannot leak
into
argus-audit.jsonorargus.log. - Has highest precedence — overrides both
<field>_envand literal<field>inargus.yml.
At most one --*-password-stdin flag per invocation (stdin is a
single stream). Argus errors out with a usage hint if more than one is
set, if stdin is a TTY, or if stdin is empty.
2. Env-var name reference (<field>_env: ENV_VAR_NAME)
scanners:
container:
registry_username_env: REGISTRY_USER
registry_password_env: REGISTRY_TOKEN
zap:
auth:
username_env: ZAP_APP_USER
password_env: ZAP_APP_PASSWORD
- The YAML holds only the name of an env var. The actual value lives in the runner's environment.
- Argus reads
os.environ[NAME]at scan time. - Recommended pattern for CI workflows — wire
${{ secrets.X }}(on GitHub) or equivalent into the runner'senv:block, then reference the env-var name fromargus.yml.
# GitHub Actions example
- name: Run argus scan
env:
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: argus scan --config argus.yml
Validator rules:
- <field>_env must be a valid POSIX shell identifier
([A-Za-z_][A-Za-z0-9_]*). Invalid names are a config error.
- Setting both <field> and <field>_env is a warning; the
_env form takes precedence.
- An unset env-var-name reference resolves to None. Argus logs a
WARNING and skips the credential — the scan still runs (graceful
degradation; useful for optional credentials).
3. Literal <field>: "value" (back-compat — discouraged)
scanners:
container:
registry_password: "literal-value-not-recommended"
Still accepted, but:
- The validator warns at config-load time if the value matches a
known vendor secret prefix (
gh*,AKIA*,AIza*, etc.). - The literal is visible to anyone reading the YAML file — including anyone with VCS read access.
This path exists for backward compatibility. New configurations
should use <field>_env exclusively.
Container image provenance
Every image argus pulls goes through one of three verification paths before the scanner is allowed to run.
Argus-owned images (cosign verify, fail-on-failure)
Four images are published from this repository to
ghcr.io/huntridge-labs/argus/*:
scanner-banditscanner-opengrepscanner-supply-chaincli
They are signed at release time via keyless Sigstore — the publishing GitHub Actions workflow obtains a short-lived certificate from Fulcio, signs the image, and the signature lands in the Rekor transparency log.
At pull time, argus runs:
cosign verify ghcr.io/huntridge-labs/argus/scanner-bandit:1.1.0 \
--certificate-identity-regexp '^https://github\.com/huntridge-labs/argus/\.github/workflows/release\.yml@' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com'
If verification fails, argus aborts the scanner with a fatal error — the scan does not run. The user gets the cosign output (truncated to 500 chars) so they can diagnose: an unsigned tag, a workflow change that hasn't been re-signed, network access to Rekor blocked, etc.
If the cosign binary is missing on PATH and verification is enabled, argus fails up front with an install hint.
Third-party images with @sha256: digest pin (trust-by-content)
# Example digest-pinned image in argus/containers.py:
"trivy": "aquasec/trivy@sha256:abcdef..."
Docker enforces the digest match at pull time — if the image's content hash doesn't equal the pinned digest, the pull itself fails. This is cryptographically equivalent to a signature check against a fixed identity, with no external trust roots needed (no Sigstore / Rekor round-trip).
Argus runs no cosign call for these images; the pull is the verification.
Third-party images with tag-only pin (warn-once)
# Most third-party images today look like this:
"trivy": "aquasec/trivy:0.70.0"
A tag pin is mutable — aquasec/trivy:0.70.0 today resolves to one
digest; tomorrow it could resolve to another if the publisher
re-tags. Argus pulls these images as-is (no extra verification
possible without a signing contract with the upstream publisher) and
emits a single WARNING per scan run listing all tag-pinned images.
We're migrating third-party images to digest pins in
argus/containers.py — Renovate keeps the digests current.
Opting out: air-gapped environments
Environments without network access to Sigstore / Rekor can't run cosign verify. Disable signature checking wholesale:
# argus.yml
execution:
verify_image_signatures: false
This skips the argus-owned image cosign verify path. Third-party digest-pin verification (which is just Docker's pull-time content hash check) continues to work because it has no external network dependency.
Alert routing — paging and escalation
argus stays out of the paging-channel business. The composite actions emit findings to:
- The job's stdout / step summary (always)
- A SARIF artifact (always)
- The PR as an aggregated comment when
post_pr_comment: true - The GitHub Security tab when
enable_code_security: true
argus does not @-mention specific users, post directly to
Slack / PagerDuty / Opsgenie, or otherwise pick a notification
channel on the team's behalf. The 0.6.x gitleaks_notify_user_list
input that routed secret-detection events through GitHub
@-mentions was removed intentionally — @-mention escalation
gets noisy fast (transient false positives ping the whole security
team on every PR), is tightly coupled to GitHub-as-notification,
and doesn't compose with the alert pipelines most teams already
own.
If you need broader-than-PR-comment escalation, pipe argus output
into your existing alert system at the workflow level. SARIF and
argus-results.json are both stable schemas; one extra workflow
step is usually enough:
- name: Run argus
uses: huntridge-labs/argus/.github/actions/scanner-gitleaks@1.1.0
id: scan
with:
enable_code_security: true
fail_on_severity: high
- name: Page on-call (only on high-severity findings)
if: failure() && steps.scan.outputs.high_count > 0
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_SECURITY_WEBHOOK }}
run: |
curl -X POST -H 'Content-Type: application/json' \
-d @argus-results.json "$SLACK_WEBHOOK_URL"
This keeps the paging decision (who, when, how loud) owned by the team's alerting config rather than the scanner's config.
What argus doesn't cover (offline VM images)
argus is a source-code and container-artifact scanner. If
your OS arrives as a container (stock base image like
ubuntu:24.04 / alpine:3.19 / redhat/ubi9, or a multi-service
/ systemd-in-container bundle), argus's container scanner already
covers it: CVE scanning via Trivy + Grype, SBOM via Syft, declared
ports via the exposure sub-scanner, and (planned) service
enumeration via the services sub-scanner.
argus does not ship offline scanning for VM-shaped artifacts — AWS AMIs, Azure VHDs, GCP disk images, on-prem VMware OVA/VMDK, ISO files, or raw disk dumps. The operational profile (libguestfs at hundreds of MB, Linux-only host, multi-minute scan latency) fundamentally doesn't match argus's PR-CI / dev-loop posture, and purpose-built tools already cover this space well. This is an explicit non-goal — see ADR-025.
If you need offline VM-image scanning, reach for these instead:
| Use case | Recommended tool |
|---|---|
| Offline mount + STIG/CIS-profile audit (any cloud, on-prem) | OpenSCAP (oscap-vm / oscap-docker) |
| AWS AMIs, no host install | AWS Inspector v2 |
| Azure VHDs / cloud workloads | Microsoft Defender for Cloud |
| GCP disk images / cloud workloads | GCP Security Command Center |
| CIS-benchmark compliance | CIS-CAT (free tier; commercial for prod use) |
| Lighter offline audit on a mounted root filesystem | Lynis |
Note: this is about the artifact shape, not the OS itself.
argus scans ubuntu:24.04 (container) fine; argus is not the right
tool for the same OS packaged as an AMI. If a Packer pipeline
produces both — scan the container artifact with argus, and the
AMI with one of the tools above.
Reporting a vulnerability
Security issues with argus itself (the CLI, SDK, MCP server, or
composite actions) should be reported via
GitHub Security Advisories on the
huntridge-labs/argus repository. Do not file a public issue.
For vulnerabilities in the scanners argus wraps (Trivy, Grype, ZAP, etc.), report upstream to the respective project.