Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
67 changes: 67 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Changelog

All notable changes to DockSec are documented in this file.

## 2026.7.4

Industry-readiness release: privacy hardening, cache correctness, waivers, and a
slimmer install.

### Added

- Secret redaction before AI analysis: secret-looking values (passwords, tokens,
API keys, private key blocks) in Dockerfiles and compose files are masked before
any content is sent to the configured LLM provider. Key names remain visible so
exposed credentials are still flagged. Opt out with `--no-redact`.
- Ignore file support (`--ignore-file`, or an auto-detected `.docksec-ignore.yml`):
suppress individual triaged findings by vulnerability or rule ID, with a required
reason and optional expiry date per entry. Suppressions apply to scoring, reports,
`--json` output, and the `--fail-on` gate.
- `--no-cache` flag to bypass the scan results cache for a run.
- Cache TTL: cached scan results now expire (default 24 hours; configurable with
`DOCKSEC_CACHE_TTL_HOURS`).
- "Data flow and privacy" documentation describing exactly what leaves the machine.
- Optional dependency extra: `pip install "docksec[ai]"` installs AI analysis
support; the base `pip install docksec` is now a slim, scan-only core with no
LLM dependencies.

- HTML report improvements: a rating badge (Excellent/Good/Fair/Poor) next to the
security score matching the terminal bands, a "Fixed In" column in the
vulnerability table, a "fix available" summary line, and a note showing how many
findings were waived via the ignore file. Waiver information also appears in the
terminal Quick take and in `--json` output (`scan_info.suppressed_count`,
`scan_info.ignore_file`).

### Changed

- AI analysis input limits raised from 50 lines / 2,000 characters to 400 lines /
16,000 characters for Dockerfiles (600 lines / 24,000 characters for compose
files), and a warning is now printed whenever input is truncated.
- Scan cache is keyed by the image content digest instead of the tag, so a rebuilt
tag (for example a reused `:latest`) never serves stale results. Full-scan cache
entries also include the Dockerfile content hash, so results are never reused
across different Dockerfiles that share an image.
- Compose rule severities tuned to reduce noise: `compose-no-non-root-user` is now
MEDIUM (was HIGH); `compose-no-resource-limits` and `compose-writable-root-fs`
are now LOW (was MEDIUM).
- `compose-port-bound-all-interfaces` now flags only sensitive ports (remote admin,
databases, caches, brokers, Docker API) instead of every published port, and now
correctly flags bare container-port entries (for example `"6379"`), which bind
0.0.0.0.
- GitHub Action usage examples now reference the pinned release tag instead of
`@main`.
- Dependency pins relaxed from exact (`==`) to compatible ranges, and unused
dependencies (pandas, tqdm, tenacity) removed.

### Fixed

- The Quick take in `--image-only` runs suggested removing `--scan-only` (the wrong
flag for that mode); it now suggests adding a Dockerfile scan.
- The Trivy progress spinner no longer prints a half-drawn progress bar into
non-terminal output such as CI logs.
- The Dockerfile scan block in the HTML report used a hardcoded light background
that was unreadable in dark mode; it now follows the report theme.

- A narrower cached scan could previously be reused in situations where the image
had been rebuilt under the same tag; digest keying fixes this class of stale
results.
67 changes: 64 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ Integrate DockSec into your GitHub Actions workflow:

```yaml
- name: Run DockSec AI Scanner
uses: OWASP/DockSec@main
uses: OWASP/DockSec@v2026.7.4
with:
dockerfile: 'Dockerfile'
openai_api_key: ${{ secrets.OPENAI_API_KEY }}
Expand All @@ -84,7 +84,10 @@ Integrate DockSec into your GitHub Actions workflow:
### CLI Usage

```bash
# Install DockSec
# Install DockSec with AI analysis support
pip install "docksec[ai]"

# Or install the slim, scan-only core (no LLM dependencies)
pip install docksec

# Scan a Dockerfile (AI-powered)
Expand Down Expand Up @@ -131,6 +134,15 @@ docksec install-skill
docksec -i myapp:latest --image-only --baseline .docksec-baseline.json --update-baseline
docksec -i myapp:latest --image-only --baseline .docksec-baseline.json --fail-on high

# Suppress triaged findings with an auditable ignore file
docksec -i myapp:latest --image-only --ignore-file .docksec-ignore.yml

# Force a fresh scan, bypassing the results cache
docksec -i myapp:latest --image-only --no-cache

# Send file content to the AI provider unmasked (secrets are redacted by default)
docksec Dockerfile --no-redact

# Reduce output to warnings, errors, and the result summary
docksec Dockerfile --scan-only --quiet

Expand Down Expand Up @@ -185,7 +197,7 @@ directly on pull requests and in the Security tab:

```yaml
- name: Run DockSec
uses: OWASP/DockSec@main
uses: OWASP/DockSec@v2026.7.4
with:
dockerfile: 'Dockerfile'
sarif: 'true'
Expand Down Expand Up @@ -274,6 +286,53 @@ valid as unrelated findings come and go. Re-run with `--update-baseline` wheneve
to accept the current state as the new baseline (e.g. after triaging and deciding to defer
a finding).

### Ignoring findings (waivers)

`--ignore-file FILE` suppresses individual findings a team has triaged and accepted.
Unlike the baseline (a point-in-time snapshot), the ignore file is an explicit,
reviewable list where every entry carries a reason and an optional expiry date.
If a `.docksec-ignore.yml` file exists in the current directory, it is picked up
automatically.

```yaml
# .docksec-ignore.yml
ignores:
- id: CVE-2023-45853 # Trivy vulnerability ID or DockSec rule ID
reason: "zlib CVE; code path not reachable, vendor fix pending"
expires: 2026-12-31 # optional; entry stops applying after this date
- id: compose-missing-healthcheck
reason: "healthchecks are handled by the orchestrator"
```

Suppressed findings are removed before scoring, reports, `--json` output, and the
`--fail-on` gate. Expired entries stop applying automatically (with a warning), and
entries without a reason are flagged so waivers stay auditable. Commit the file to
version control so suppressions are reviewed like any other change.

### Data flow and privacy

DockSec is designed so you always know what leaves your machine:

- **Scanning is fully local.** Trivy, Hadolint, and the security score run on your
machine. Image contents are never uploaded anywhere by DockSec.
- **AI analysis sends only the scanned file.** When the AI pass runs, the Dockerfile
or compose file content (plus a short summary of vulnerability counts for scoring)
is sent to the LLM provider you configured. Nothing else is transmitted.
- **Secrets are redacted before they leave.** Secret-looking values (passwords,
tokens, API keys, private key blocks) in the file are masked before the content is
sent to the AI provider. Key names stay visible so exposed credentials are still
flagged. Use `--no-redact` to opt out.
- **Fully local AI is supported.** Use `--provider ollama` to keep the AI analysis on
your own hardware, or `--scan-only` / `--offline` to skip AI entirely.
- **No telemetry.** DockSec collects no usage data and phones home to nothing.

### Scan results cache

Image scan results are cached (default: 24 hours, override with
`DOCKSEC_CACHE_TTL_HOURS`) and keyed by the image's content digest, so a rebuilt tag
such as a reused `:latest` always gets a fresh scan. Use `--no-cache` (or
`DOCKSEC_USE_CACHE=false`) to bypass the cache for a run.

### Exit codes

DockSec uses CI-friendly exit codes so builds and shells can react to results:
Expand Down Expand Up @@ -302,6 +361,8 @@ severity is widened automatically so the gate can observe those findings.
- **Rich Formats**: Professional exports in HTML (interactive), PDF, JSON, CSV, SARIF, and CycloneDX SBOM.
- **Supply-Chain Ready**: Generate a CycloneDX SBOM (`--sbom`) of any image for Dependency-Track and other consumers.
- **Offline Mode**: Scan fully air-gapped (`--offline`) using the local Trivy database, no network required.
- **Privacy First**: Secret values are redacted before any content reaches an AI provider, scanning is fully local, and there is no telemetry.
- **Auditable Waivers**: Suppress triaged findings with an ignore file that records a reason and expiry date per entry.
- **CI/CD Ready**: Designed for easy integration into GitHub Actions and build pipelines.
- **AI-Assistant Skills**: `docksec install-skill` teaches Claude Code, Cursor, Copilot, and others how to run DockSec in your repo.
- **GitHub Action**: Available on the GitHub Marketplace for automated security scans.
Expand Down
68 changes: 65 additions & 3 deletions docksec/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ def main() -> None:
parser.add_argument('--sarif', dest='sarif', action='store_true', help='Write a SARIF 2.1.0 report for GitHub Code Scanning and other SARIF-compatible tools')
parser.add_argument('--sbom', dest='sbom', action='store_true', help='Write a CycloneDX SBOM (.cdx.json) of the scanned image for supply-chain tooling (requires an image)')
parser.add_argument('--offline', dest='offline', action='store_true', help='Run without network access: use the local Trivy DB (no DB update) and skip AI analysis')
parser.add_argument('--no-redact', dest='no_redact', action='store_true', help='Do not mask secret-looking values before sending file content to the AI provider')
parser.add_argument('--no-cache', dest='no_cache', action='store_true', help='Bypass the scan results cache and force a fresh scan')
parser.add_argument('--ignore-file', dest='ignore_file', metavar='FILE', help='Path to an ignore file listing findings to suppress (default: .docksec-ignore.yml in the current directory, if present)')
parser.add_argument('--baseline', dest='baseline', metavar='FILE', help='Path to a baseline file; with --fail-on, only findings not present in the baseline trigger the gate')
parser.add_argument('--update-baseline', dest='update_baseline', action='store_true', help='Write the current scan findings to --baseline instead of gating against it')
parser.add_argument('--quiet', action='store_true', help='Reduce output to warnings, errors, and the result summary')
Expand Down Expand Up @@ -100,6 +103,11 @@ def main() -> None:
if args.compact_output:
os.environ["DOCKSEC_COMPACT_OUTPUT"] = "true"

# --no-cache: the scanner (and every per-service scanner in compose runs)
# reads DOCKSEC_USE_CACHE at construction time.
if args.no_cache:
os.environ["DOCKSEC_USE_CACHE"] = "false"

if args.verbose and not os.getenv("DOCKSEC_LOG_LEVEL"):
os.environ["DOCKSEC_LOG_LEVEL"] = "INFO"

Expand Down Expand Up @@ -321,8 +329,29 @@ def main() -> None:
output.error(f"No {file_type} content found.")
return

# Truncate content to reduce token usage
truncated_content = truncate_dockerfile(filecontent, max_lines=150, max_chars=4000) if run_compose_analysis else truncate_dockerfile(filecontent, max_lines=50, max_chars=2000)
# Redact secret-looking values before the content leaves the
# machine. Keys stay visible so the model can still flag exposed
# credentials; the secret material itself is masked.
if not args.no_redact:
from docksec.redact import redact_content
filecontent, redacted_count = redact_content(filecontent)
if redacted_count:
output.info(
f"Masked {redacted_count} secret-looking value(s) before AI analysis "
f"(--no-redact to disable)"
)

# Cap very large inputs to bound token usage; warn when anything
# is dropped so a partial analysis is never mistaken for a full one.
if run_compose_analysis:
truncated_content = truncate_dockerfile(filecontent, max_lines=600, max_chars=24000)
else:
truncated_content = truncate_dockerfile(filecontent, max_lines=400, max_chars=16000)
if truncated_content != filecontent:
output.warn(
f"{file_type} is very large; AI analysis covers only the first part "
f"of the file. Scanner results (Trivy/Hadolint) are unaffected."
)

response = analyser_chain.invoke({"filecontent": truncated_content})
ai_findings = analyze_security(response, compact=True, report_path=output_dir)
Expand Down Expand Up @@ -379,6 +408,29 @@ def main() -> None:
# Full scan including Dockerfile
results = scanner.run_full_scan(severity)

# Apply ignore-file suppressions before scoring, reports, JSON
# output, and the --fail-on gate see the findings.
ignore_path = args.ignore_file
if not ignore_path:
from docksec.ignore import find_default_ignore_file
ignore_path = find_default_ignore_file()
if ignore_path:
from docksec.ignore import load_ignore_file, apply_ignores
ignore_entries, ignore_warnings = load_ignore_file(ignore_path)
for warning in ignore_warnings:
output.warn(warning)
kept_findings, suppressed_count = apply_ignores(
results.get("json_data", []), ignore_entries)
if suppressed_count:
results["json_data"] = kept_findings
# Recorded so the reports and JSON payload can state that
# findings were waived rather than silently absent.
results["suppressed_count"] = suppressed_count
results["ignore_file"] = ignore_path
output.info(
f"{suppressed_count} finding(s) suppressed by ignore file {ignore_path}"
)

# Calculate security score
scanner.analysis_score = scanner.get_security_score(results)

Expand Down Expand Up @@ -566,6 +618,9 @@ def _print_json_results(results, scanner, report_paths):
"vulnerabilities": vulnerabilities,
"severity_counts": output.count_by_severity(vulnerabilities),
}
if results.get("suppressed_count"):
payload["scan_info"]["suppressed_count"] = results["suppressed_count"]
payload["scan_info"]["ignore_file"] = results.get("ignore_file")
if "ai_findings" in results:
payload["ai_analysis"] = results["ai_findings"]
if report_paths:
Expand Down Expand Up @@ -620,8 +675,15 @@ def _quick_take_lines(results, counts, run_ai):
if exposed:
lines.append(f"{len(exposed)} likely exposed credential(s) flagged by AI analysis")

suppressed = results.get("suppressed_count")
if suppressed:
lines.append(f"{suppressed} triaged finding(s) suppressed via ignore file")

if not run_ai and not results.get("ai_findings"):
lines.append("Run without --scan-only to add AI-powered explanations and fixes")
if results.get("scan_mode") == "image_only":
lines.append("Add a Dockerfile scan for AI-powered explanations and fixes: docksec <Dockerfile> -i <image>")
else:
lines.append("Run without --scan-only to add AI-powered explanations and fixes")

return lines

Expand Down
Loading
Loading