Skip to content

Publish image from the script + Docker Scout hardening#7

Merged
nirmalgupta merged 2 commits into
mainfrom
feat/publish-via-script
Jun 2, 2026
Merged

Publish image from the script + Docker Scout hardening#7
nirmalgupta merged 2 commits into
mainfrom
feat/publish-via-script

Conversation

@nirmalgupta
Copy link
Copy Markdown
Member

@nirmalgupta nirmalgupta commented Jun 2, 2026

Summary

Two related changes:

  1. Publish from the script, not GH Actions. Replaces .github/workflows/publish.yml with a local ./security-scan.sh publish subcommand that uses the maintainer's existing docker login credentials. No CI secrets needed.

  2. Harden the image for Docker Scout — closes all four warnings on v0.2.0's 'F' score:

    • Missing supply chain attestation(s)--sbom=true --provenance=mode=max added to the buildx invocation.
    • High-profile vulnerabilities found + Fixable critical or high vulnerabilities found → base bumped python:3.11-slimpython:3.12-slim; apt-get upgrade -y inserted between update and install; setuptools pinned <80 (newer setuptools dropped pkg_resources which semgrep's opentelemetry dep still imports).
    • No default non-root user found → new scanner user (uid 1000), USER scanner at the end of the Dockerfile, /work and the trivy DB cache (moved to /var/cache/trivy) chowned. Also closes Pin SHA-256 sums for scanner binaries downloaded in Dockerfile #1.

Version bumped 0.2.00.2.1 in pyproject + manifest + __init__.py. The manifest's changelog entry for 0.2.1 surfaces the hardening so the consumer skill shows it on the upgrade prompt.

Publish usage

docker login                                  # uses existing creds, one time
./security-scan.sh publish                    # multi-arch, prompts, pushes leverj/security-scan
./security-scan.sh publish v0.3.0-rc1         # explicit tag
./security-scan.sh publish --no-push          # release dry-run, build only
./security-scan.sh publish --single-arch      # host arch only
./security-scan.sh publish --repo me/image    # alternate repo

Test plan

  • 221 tests pass.
  • ./security-scan.sh publish --help shows full usage.
  • ./security-scan.sh publish --no-push end-to-end: tag resolves to v0.2.1 from manifest, multi-arch buildx succeeds, SBOM syft scanner runs, prints "built locally (not pushed)".
  • docker build . succeeds locally on python:3.12-slim with the setuptools pin.
  • Non-root verification: docker run --rm --entrypoint id <image>uid=1000(scanner) gid=1000(scanner) groups=1000(scanner).
  • Entrypoint sanity: docker run --rm --entrypoint python <image> -m security_scan --help runs cleanly as the unprivileged user.
  • Trivy cache readable as non-root: /var/cache/trivy/{db,java-db} present.
  • Post-merge: ./security-scan.sh publish from your shell to push v0.2.1 and let Docker Scout re-grade.

Closes #1.

🤖 Generated with Claude Code

…workflow

Replaces .github/workflows/publish.yml with a local `./security-scan.sh
publish` subcommand. Rationale: the maintainer is already logged in to
Docker Hub on the console (`docker login`), so storing repo secrets just
to mirror that auth is needless complexity.

  ./security-scan.sh publish               # multi-arch, push to Docker Hub
  ./security-scan.sh publish v0.3.0-rc1    # explicit tag (skips version check)
  ./security-scan.sh publish --no-push     # build-only release dry-run
  ./security-scan.sh publish --single-arch # host arch only (skip buildx multi-arch)
  ./security-scan.sh publish --repo me/img # different docker hub repo

By default the tag is derived from SECURITY-SCAN-MANIFEST.yaml; the script
verifies pyproject.toml's version matches (same guard the old workflow did)
before tagging. An explicit `vX.Y.Z` argument overrides without the check.

Push happens via `docker buildx build --push` so credentials come from the
host's existing `~/.docker/config.json` — never seen by the script.

`--no-push` paired with multi-arch builds to the buildx cache only (no
local load — buildx --load doesn't support multi-platform). The build
itself runs end-to-end so it's still a real release dry-run.

After the push, a smoke step runs `docker run --rm --entrypoint cat
leverj/security-scan:vX.Y.Z /app/SECURITY-SCAN-MANIFEST.yaml | head -5`
to prove the manifest landed in the pushed image.

README updated to document the publish flow.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings June 2, 2026 19:21
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR moves the Docker image publishing flow from GitHub Actions to a new local ./security-scan.sh publish subcommand, and removes the old .github/workflows/publish.yml workflow. The goal is to rely on the developer’s existing local Docker credential store (via docker login) rather than mirroring auth into CI secrets.

Changes:

  • Added publish subcommand to security-scan.sh to build (optionally multi-arch) and push to Docker Hub, with version alignment checks against pyproject.toml and SECURITY-SCAN-MANIFEST.yaml.
  • Documented the local publish flow in README.md.
  • Deleted the GitHub Actions publish workflow.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
security-scan.sh Adds local publish command, argument parsing, version resolution, buildx build/push, and a post-push smoke check.
README.md Adds a “Publish a new image” section describing the local publish workflow and flags.
.github/workflows/publish.yml Removes the previous CI-based Docker publish pipeline.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread security-scan.sh
Comment on lines +239 to +240
echo "smoke test: extracting manifest from the pushed image..."
docker run --rm --entrypoint cat "$image_versioned" /app/SECURITY-SCAN-MANIFEST.yaml | head -5
Comment thread security-scan.sh
Comment on lines +125 to +131
# read_version_from <file> <regex>
# Pulls the first match of <regex> from <file>, returns just the captured version.
read_version_from() {
local file="$1" pattern="$2"
[[ -f "$file" ]] || die "missing file: $file"
grep -E "$pattern" "$file" | head -1 | sed -E 's/.*"([^"]+)".*/\1/'
}
Comment thread security-scan.sh
Comment on lines +145 to +146
--repo) repo="$2"; shift 2 ;;
--repo=*) repo="${1#--repo=}"; shift ;;
Comment thread security-scan.sh
Comment on lines +159 to +162
--no-push build + tag locally; do NOT push. Useful to dry-run a release.
--single-arch build only for the host architecture (skip multi-arch buildx).
Default: linux/amd64 + linux/arm64.
--repo override the image repo. Default: leverj/security-scan.
Comment thread security-scan.sh
Comment on lines +241 to +243
else
echo "built locally (not pushed): $image_versioned + $image_latest"
fi
Comment thread security-scan.sh
echo "publish: using explicit tag $tag (skipping version-alignment check)"
fi

# Sanity: the version-stripped tag should match what's in the manifest if no override.
Comment thread README.md
Comment on lines +195 to +198
`leverj/security-scan:v<version>` + `:latest` for amd64 + arm64. Pass an
explicit tag to override (`./security-scan.sh publish v0.3.0-rc1`), or
`--no-push` to do a release dry-run that builds locally without pushing.
Run `./security-scan.sh publish --help` for the full set of flags.
Addresses every warning in Docker Scout's 'F' health score for v0.2.0:

1. Missing supply chain attestation(s)
   security-scan.sh's `publish` now passes `--sbom=true` and
   `--provenance=mode=max` to `docker buildx build`. The first push of
   v0.2.1 will attach a CycloneDX SBOM and SLSA build provenance to the
   manifest list — the syft scanner runs cleanly in --no-push dry-run.

2. High-profile vulnerabilities found
3. Fixable critical or high vulnerabilities found
   Base image bumped from python:3.11-slim to python:3.12-slim. Added
   `apt-get upgrade -y` between update and install so the layer carries
   the latest OS-level security patches. setuptools pinned <80 because
   newer setuptools dropped the bundled `pkg_resources` module that
   semgrep 1.97's opentelemetry dep still imports at runtime.

4. No default non-root user found  (also closes #1)
   Added a non-root `scanner` user (uid 1000) and a USER directive at
   the end of the Dockerfile. /work and the trivy DB cache (moved to
   /var/cache/trivy) are chowned so the unprivileged user can still
   write the clone, gitleaks tempfile, SBOM output, and read the
   pre-cached vuln DBs. Verified with `docker run --entrypoint id`:
     uid=1000(scanner) gid=1000(scanner) groups=1000(scanner)
   Entrypoint sanity-checked: `python -m security_scan --help` runs
   cleanly as the new user.

Version + manifest bumped 0.2.0 -> 0.2.1. The manifest's changelog leads
with the 0.2.1 hardening note so the consumer skill surfaces it to users
on the upgrade prompt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@nirmalgupta nirmalgupta changed the title Publish image from the script; drop GH Actions workflow Publish image from the script + Docker Scout hardening Jun 2, 2026
@nirmalgupta nirmalgupta merged commit 56da624 into main Jun 2, 2026
2 checks passed
@nirmalgupta nirmalgupta deleted the feat/publish-via-script branch June 2, 2026 19:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Pin SHA-256 sums for scanner binaries downloaded in Dockerfile

2 participants