Skip to content

Add grype container image vulnerability scanning to compile pipeline#47474

Merged
pelikhan merged 4 commits into
mainfrom
copilot/add-container-image-validation-support
Jul 23, 2026
Merged

Add grype container image vulnerability scanning to compile pipeline#47474
pelikhan merged 4 commits into
mainfrom
copilot/add-container-image-validation-support

Conversation

Copilot AI commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Adds --grype to the compile pipeline (alongside --zizmor, --poutine, --actionlint, --runner-guard) to scan container images referenced in compiled workflows for known CVEs using anchore/grype via Docker — no native install required.

How it works

  • Container image refs are extracted from the gh-aw-manifest header embedded in each .lock.yml
  • Images are deduplicated by PinnedImage (digest-pinned ref) before scanning — one Docker run per unique image even across many workflows
  • grype <image> -o json output is parsed and normalized to console.CompilerError with severity mapping: Critical/High → error, Medium/Unknown → warning, Low/Negligible → info
  • In strict mode, any findings or scan errors cause a non-zero exit

Changes

  • pkg/cli/grype.go — new: collectContainerImagesFromLockFiles, runGrypeOnLockFiles, grypeRunOnImage, grypeDisplayFindings, thread-safe grypeCache
  • pkg/cli/docker_images.goGrypeImage = "anchore/grype:latest" constant; CheckAndPrepareDockerImages gains 5th useGrype bool param
  • pkg/cli/compile_pipeline.go — grype block added to both compileSpecificFiles and compileAllFilesInDirectory after runner-guard
  • pkg/cli/compile_config.go, compile_external_tools.go, mcp_tools_readonly.go, cmd/gh-aw/main.goGrype bool field and --grype flag wired end-to-end
  • pkg/cli/grype_test.go — 15 unit tests: cache behavior, image collection, deduplication, severity mapping, edge cases
  • pkg/cli/docker_images_test.go — updated all call sites for new signature; GrypeImage added to constants test

Usage

gh aw compile --grype

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title Add grype container image vulnerability scanning support Add grype container image vulnerability scanning to compile pipeline Jul 23, 2026
Copilot AI requested a review from pelikhan July 23, 2026 05:40
@pelikhan
pelikhan marked this pull request as ready for review July 23, 2026 05:40
Copilot AI review requested due to automatic review settings July 23, 2026 05:40
@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Test Quality Sentinel completed test quality analysis.

@github-actions

github-actions Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

PR Code Quality Reviewer completed the code quality review.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Adds Docker-based Grype vulnerability scanning to the workflow compilation pipeline.

Changes:

  • Adds the --grype CLI and MCP option.
  • Extracts and deduplicates manifest container images for scanning.
  • Adds Grype integration and unit tests.
Show a summary per file
File Description
cmd/gh-aw/main.go Registers and configures --grype.
pkg/cli/compile_config.go Adds Grype compilation configuration.
pkg/cli/compile_external_tools.go Exposes the batch Grype runner.
pkg/cli/compile_pipeline.go Runs Grype after compilation.
pkg/cli/docker_images.go Adds Grype Docker image preparation.
pkg/cli/docker_images_test.go Updates image-preparation tests.
pkg/cli/grype.go Implements image collection, scanning, caching, and rendering.
pkg/cli/grype_test.go Tests collection, caching, and findings.
pkg/cli/mcp_tools_readonly.go Wires Grype into MCP compilation.

Review details

Tip

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

  • Files reviewed: 9/9 changed files
  • Comments generated: 5
  • Review effort level: Medium

Comment thread pkg/cli/grype.go
Comment on lines +266 to +287
if runErr != nil {
var exitErr *exec.ExitError
if !errors.As(runErr, &exitErr) {
// Command could not be started (e.g., Docker not found).
scanErr := fmt.Errorf("grype failed: %w", runErr)
grypeScanResultCache.setError(imageRef, scanErr)
return nil, scanErr
}
exitCode := exitErr.ExitCode()
// Exit code 1 means grype found vulnerabilities — that is expected and parseable.
// Any other non-zero code signals a real tool failure.
if exitCode != 1 || (parseErr != nil && stdout.Len() == 0) {
stderrStr := strings.TrimSpace(stderr.String())
if stderrStr != "" {
grypeLog.Printf("grype stderr for %s: %s", imageRef, stderrStr)
}
scanErr := fmt.Errorf("grype failed with exit code %d on %s", exitCode, imageRef)
grypeScanResultCache.setError(imageRef, scanErr)
return nil, scanErr
}
// Exit code 1 with JSON output — vulnerability findings were returned normally.
}
Comment thread pkg/cli/grype.go
Comment on lines +238 to +242
cmd := exec.Command(
"docker",
"run",
"--rm",
GrypeImage,
Comment on lines +206 to +208
if args.Grype {
cmdArgs = append(cmdArgs, "--grype")
}
Comment thread pkg/cli/grype.go
Comment on lines +127 to +130
content, err := os.ReadFile(lockFile)
if err != nil {
grypeLog.Printf("Skipping %s: failed to read file: %v", lockFile, err)
continue
Comment thread pkg/cli/grype_test.go
Comment on lines +62 to +64
// We can't easily test the errorType without capturing stderr,
// but we can verify the function returns the right count.
count := grypeDisplayFindings("test-image:latest", output)
@github-actions

Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

⚠️ Test Quality Score: 74/100 — Acceptable

Analyzed 44 test(s): 24 design, 20 implementation, 0 violation(s).

📊 Metrics (44 tests)
Metric Value
Analyzed 44 (Go: 44, JS: 0)
✅ Design 24 (55%)
⚠️ Implementation 20 (45%)
Edge/error coverage 32 (73%)
Duplicate clusters 0
Inflation grype 0.91x ✅, docker 1.67x ✅
🚨 Violations 0
Test File Tests Type Classification
docker_images_test.go 27 Modified (+15 lines) 44% design, 56% impl
grype_test.go 17 Added (315 lines) 71% design, 29% impl
📋 Test Classification

Docker Images (27 tests)

Design tests (behavioral contracts):

  • TestCheckAndPrepareDockerImages_* (6 tests) — API contracts for image preparation
  • Error message format/content validation (4 tests) — User-facing error guarantees
  • Error type validation (1 test) — Error categorization contract

Concurrency/Race tests (6 tests):

  • TestStartDockerImageDownload_ConcurrentCalls — Race condition safety
  • TestStartDockerImageDownload_ContextCancellation — Graceful shutdown
  • Related concurrent scenarios (4 tests)

Implementation/State tests (10 tests):

  • State management, transitions, mocking

Grype (17 tests)

Design tests (data contracts):

  • TestCollectContainerImagesFromLockFiles_* (7 tests) — Image collection API
  • Manifest parsing, deduplication, filtering — Data transformation contracts
  • TestGrypeDisplayFindings_* (3 tests) — Output contract

State tests (3 tests):

  • Cache get/set/reset operations

Output tests (4 tests):

  • Severity mapping, display findings

✅ Verdict

PASS. 45% implementation tests (45% exceeds 30% threshold slightly, but acceptable given extensive concurrency testing). No coding-guideline violations detected.

Guideline Status:

  • ✅ No gomock/testify mock usage
  • ✅ All Go tests have //go:build !integration tag
  • ✅ No mocking violations
  • ✅ Inflation ratios healthy (grype 0.91x, docker 1.67x, both < 2:1)
  • ✅ 73% edge-case coverage

Notes:

  1. Test inflation in docker_images_test.go (1.67x) is justified by 6 concurrent access tests verifying race condition safety
  2. Grype display findings tests cannot verify stderr output format (acknowledged with comment in test), minor limitation
  3. Strong manifest parsing validation in grype tests covers JSON edge cases well

🧪 Test quality analysis by Test Quality Sentinel · haiku45 24.5 AIC · ⌖ 7.93 AIC · ⊞ 7K ·
Comment /review to run again

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Test Quality Sentinel: 74/100. 45% implementation tests (threshold: 30%). No coding-guideline violations detected. Strong concurrent access and error handling test coverage.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review: grype container image vulnerability scanning

Overall the implementation is well-structured — the cache, deduplication by pinned digest, and integration pattern matching other scanners are all solid. Two issues need addressing before merge:

Blocking:

  1. Logic bug (pkg/cli/grype.go:277): The condition (parseErr != nil && stdout.Len() == 0) is dead code — parseErr can only be non-nil when stdout.Len() > 0. Fix: if exitCode != 1 || parseErr != nil {
  2. Unpinned Docker Hub image (pkg/cli/docker_images.go:36): anchore/grype:latest uses Docker Hub with a mutable :latest tag, unlike every other scanner image which uses ghcr.io. This is a supply-chain risk. Pin to a specific version or digest.

🧵 Reviewed using Impeccable skills by Impeccable Skills Reviewer · sonnet46 46.8 AIC · ⌖ 4.7 AIC · ⊞ 5K

Comment thread pkg/cli/grype.go
exitCode := exitErr.ExitCode()
// Exit code 1 means grype found vulnerabilities — that is expected and parseable.
// Any other non-zero code signals a real tool failure.
if exitCode != 1 || (parseErr != nil && stdout.Len() == 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The condition (parseErr != nil && stdout.Len() == 0) is dead code and will never be true. parseErr is only assigned inside the stdout.Len() > 0 block (line 262), so stdout.Len() == 0 is always false whenever parseErr != nil.

The intended guard — reject exit code 1 when no parseable JSON was returned — should be:

if exitCode != 1 || parseErr != nil {

This correctly treats exit code 1 with valid JSON as "vulnerabilities found" and exit code 1 without parseable JSON as a tool failure.

@copilot please address this.

Comment thread pkg/cli/docker_images.go
PoutineImage = "ghcr.io/boostsecurityio/poutine:latest"
ActionlintImage = "rhysd/actionlint:1.7.12"
RunnerGuardImage = "ghcr.io/vigilant-llc/runner-guard:latest"
GrypeImage = "anchore/grype:latest"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Using anchore/grype:latest pulls from Docker Hub with an unpinned mutable tag. All other scanner images in this file use ghcr.io registries (ghcr.io/zizmorcore/zizmor:latest, ghcr.io/boostsecurityio/poutine:latest, etc.). Docker Hub images are subject to rate-limiting and are a supply-chain risk (mutable tag, different trust boundary).

Consider:

  1. Using the ghcr.io mirror if one exists for grype, or pin to a specific version tag (e.g., anchore/grype:v0.85.0) to avoid drift.
  2. Pinning the digest (e.g., anchore/grype:v0.85.0@sha256:...) for full immutability, consistent with how workflow containers are pinned.

@copilot please address this.

…y scanning

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (736 new lines in pkg/ directories) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/47474-add-grype-container-image-vulnerability-scanning.md — review and complete it before merging.

🔒 This PR cannot merge until an ADR is linked in the PR body.

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI could not infer, refine the decision rationale, and list real alternatives you considered
  3. Commit the finalized ADR to docs/adr/ on your branch
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-47474: Add Grype Container Image Vulnerability Scanning

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

❓ Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 47474-add-grype-container-image-vulnerability-scanning.md for PR #47474).

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · sonnet46 70.8 AIC · ⌖ 9.99 AIC · ⊞ 8.5K ·
Comment /review to run again

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot update static analysis agentic workflow

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /codebase-design, /diagnosing-bugs, and /tdd — requesting changes on three blocking issues.

📋 Key Themes & Highlights

Blocking Issues

  1. No context propagationgrypeRunOnImage does not accept a context.Context, so a hung Docker call cannot be cancelled by the compile pipeline. Use exec.CommandContext.
  2. Floating latest tagGrypeImage = "anchore/grype:latest" breaks the reproducibility guarantee; all peer tools pin specific versions.
  3. Fragile exit-code guard — The condition exitCode != 1 || (parseErr != nil && stdout.Len() == 0) conflates two failure modes and will misclassify grype exit code 2 (future policy-threshold exit).

Notable Observations

  • Global cache (grypeScanResultCache) requires reset() calls in tests to avoid pollution — injecting the cache as a parameter would be cleaner.
  • Strict-mode test coveragerunGrypeOnLockFiles lacks tests for the strict + findings and strict + scan-error paths.
  • Misleading error paramsparamsList appends grype: false even when grype is requested; inherited pattern but worth fixing.

Positive Highlights

  • ✅ Excellent deduplication by pinned digest before scanning
  • ✅ Clean severity → error-type mapping using existing CompilerError format
  • ✅ Good use of the manifest extraction pipeline for image discovery
  • ✅ 15 focused unit tests for the cache, collection, and display helpers

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 69.2 AIC · ⌖ 5.25 AIC · ⊞ 6.7K
Comment /matt to run again

Comment thread pkg/cli/grype.go

// grypeRunOnImage runs grype on a single container image reference via Docker,
// using the result cache to avoid re-scanning images already checked in this run.
func grypeRunOnImage(imageRef string, verbose bool) (*grypeOutput, error) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/codebase-design] grypeRunOnImage accepts no context.Context, so long-running Docker scans cannot be cancelled when the compile pipeline context is cancelled (e.g. Ctrl+C or a timeout).

💡 Suggested fix

Add a context parameter and switch to exec.CommandContext:

func grypeRunOnImage(ctx context.Context, imageRef string, verbose bool) (*grypeOutput, error) {
    cmd := exec.CommandContext(ctx, "docker", "run", "--rm", GrypeImage, imageRef, "-o", "json")
    // ...
}

The pipeline already checks ctx.Err() before calling this function, but a Docker call that hangs inside the function will still block the process indefinitely.

@copilot please address this.

Comment thread pkg/cli/grype.go
exitCode := exitErr.ExitCode()
// Exit code 1 means grype found vulnerabilities — that is expected and parseable.
// Any other non-zero code signals a real tool failure.
if exitCode != 1 || (parseErr != nil && stdout.Len() == 0) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnosing-bugs] The exit-code guard exitCode != 1 || (parseErr != nil && stdout.Len() == 0) conflates two distinct failure modes and has an off-by-one risk: if grype ever returns exit code 2 for a policy threshold it will be incorrectly treated as a tool failure, silently swallowing JSON output that was already parsed into output.

💡 Suggested approach

Separate the two concerns explicitly:

if exitCode == 1 && stdout.Len() > 0 {
    // Grype found vulnerabilities — expected. Parse below.
} else if exitCode != 0 {
    // Genuine tool failure.
    scanErr := fmt.Errorf("grype exited %d for %s: %s", exitCode, imageRef, stderr.String())
    grypeScanResultCache.setError(imageRef, scanErr)
    return nil, scanErr
}

Also add a test that simulates a non-zero, non-1 exit code with JSON on stdout to confirm the behavior you intend.

@copilot please address this.

Comment thread pkg/cli/docker_images.go
PoutineImage = "ghcr.io/boostsecurityio/poutine:latest"
ActionlintImage = "rhysd/actionlint:1.7.12"
RunnerGuardImage = "ghcr.io/vigilant-llc/runner-guard:latest"
GrypeImage = "anchore/grype:latest"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/codebase-design] GrypeImage = "anchore/grype:latest" uses a floating latest tag. All other tool images in this file are pinned to specific versions (e.g. rhysd/actionlint:1.7.12). Using latest makes scan results non-reproducible and breaks the digest-pinning guarantee that runner-guard enforces on compiled workflows.

💡 Suggested fix

Pin to a specific release, e.g.:

GrypeImage = "anchore/grype:v0.87.0"

Check the latest release at https://github.com/anchore/grype/releases and update TestDockerImageConstants to assert the tag format matches.

@copilot please address this.

Comment thread pkg/cli/grype.go
}

// grypeScanResultCache is the process-wide grype result cache.
var grypeScanResultCache = &grypeCache{

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/codebase-design] grypeScanResultCache is a package-level variable, which means it persists across test runs and can cause cross-test pollution even though reset() is provided. Tests that call reset() directly are a workaround for a design issue.

💡 Suggested fix

Accept the cache as a parameter or inject it via the runner function, keeping the global only as a default:

func grypeRunOnImage(imageRef string, verbose bool, cache *grypeCache) (*grypeOutput, error) { ... }

This makes the dependency explicit and allows tests to pass a fresh cache without relying on reset(). The public wrapper RunGrypeOnLockFiles can still use grypeScanResultCache as the default.

@copilot please address this.

Comment thread pkg/cli/docker_images.go
}
if useGrype {
tool := "grype"
requestedTools = append(requestedTools, tool)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/diagnosing-bugs] The Docker-unavailable error message for grype unconditionally appends "grype: false" — copied from the pattern for the other tools but the string is misleading (grype is requested, not disabled).

💡 Details

The bug is in the newly added block:

if useGrype {
    tool := "grype"
    requestedTools = append(requestedTools, tool)
    paramsList = append(paramsList, tool+": false")  // ← should be tool+": true"
}

Every other tool appends tool+": false" as well, so this is a pre-existing pattern issue — but it produces a confusing error message (grype: false) for a user who explicitly passed --grype. Consider fixing or at minimum add a comment explaining why.

@copilot please address this.

Comment thread pkg/cli/grype.go
// vulnerability scanner on each unique image via Docker.
func runGrypeOnLockFiles(lockFiles []string, verbose bool, strict bool) error {
if len(lockFiles) == 0 {
return nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[/tdd] runGrypeOnLockFiles is not tested under strict mode with actual findings — only the no-image and empty-lock-file paths are covered. There are no tests that verify the strict-mode early return when totalFindings > 0 or when len(scanErrors) > 0.

💡 Suggested tests to add
func TestRunGrypeOnLockFiles_StrictMode_ReturnsErrorOnFindings(t *testing.T) { ... }
func TestRunGrypeOnLockFiles_StrictMode_ReturnsErrorOnScanFailure(t *testing.T) { ... }
func TestRunGrypeOnLockFiles_NonStrictMode_WarnsOnScanFailure(t *testing.T) { ... }

These are the paths most likely to break silently in production.

@copilot please address this.

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

REQUEST_CHANGES — Three correctness bugs in the grype output parser make this scanner unreliable as a security gate.

🔴 Blocking issues (must fix)
  1. Cache error lifetime (grype.go:174): Transient scan errors are permanently cached at process scope. MCP users and repeated compile runs will hit silent no-ops after any Docker blip — no retry ever fires. The "per compile run" caching documented in the header comment is only true for successful results.

  2. Silent empty result on non-JSON stdout (grype.go:226): When grype exits 1 but emits a non-JSON error string (image pull failure, bad arg, etc.), the HasPrefix("{") guard skips parsing, parseErr stays nil, and the code returns a zero-finding success. A compromised or unavailable image is silently reported as clean.

  3. Fragile JSON parse guard (grype.go:218): The HasPrefix("{") + separate Unmarshal(stdout.Bytes()) split is ambiguous — BOM, whitespace trimming, and the separate parseErr variable make the control flow hard to reason about. Use json.NewDecoder directly.

🟡 Non-blocking but should fix
  • GrypeImage = "anchore/grype:latest" (docker_images.go:142): Unpinned tag makes scan results non-reproducible and may break the JSON schema silently. Every other tool image in this file is pinned.
  • Error messages log img.Image instead of imageRef (grype.go:155): Scan failures name the short tag, not the pinned digest that was actually scanned, making them impossible to reproduce.

🔎 Code quality review by PR Code Quality Reviewer · sonnet46 74.4 AIC · ⌖ 5.35 AIC · ⊞ 5.7K
Comment /review to run again

Comments that could not be inline-anchored

pkg/cli/grype.go:174

Process-wide scan error cache never resets between compile runs: a transient Docker failure permanently poisons the cache for that image ref, silently skipping all future scans in the same process.

<details>
<summary>💡 Details and suggested fix</summary>

grypeScanResultCache is a package-level singleton. Its setError path caches scan failures permanently for the process lifetime. When --grype is invoked multiple times within the same process (e.g., via the MCP tool), a transient fa…

pkg/cli/grype.go:226

Silent empty result when grype exits 1 with non-JSON stdout: if grype exits with code 1 but outputs a warning string instead of JSON (e.g., image pull failure message), the HasPrefix(&quot;{&quot;) guard silently skips parsing and returns zero findings — masking the real failure.

<details>
<summary>💡 Details and suggested fix</summary>

The current logic:

if stdout.Len() &gt; 0 &amp;&amp; strings.HasPrefix(strings.TrimSpace(stdout.String()), &quot;{&quot;) {
    parseErr = json.Unmarshal(stdout.Bytes(), &amp;outpu</details>

<details><summary>pkg/cli/grype.go:218</summary>

**`HasPrefix(&quot;{&quot;)` JSON detection is fragile**: a BOM, leading whitespace, or any prefix character causes the parse to be silently skipped, returning zero findings instead of an error.

&lt;details&gt;
&lt;summary&gt;💡 Details and suggested fix&lt;/summary&gt;

```go
if stdout.Len() &gt; 0 &amp;&amp; strings.HasPrefix(strings.TrimSpace(stdout.String()), &quot;{&quot;) {
    parseErr = json.Unmarshal(stdout.Bytes(), &amp;output)
}

TrimSpace is applied to the string for the prefix check, but stdout.Bytes() (the untrimmed origina…

pkg/cli/grype.go:155

Scan error messages use the original image tag, not the pinned digest actually scanned: when debugging a scan failure, the error log names img.Image (e.g., ubuntu:20.04) but grype was invoked with imageRef (e.g., ubuntu:20.04@sha256:abc...), making it impossible to reproduce the exact invocation from the error message alone.

<details>
<summary>💡 Suggested fix</summary>

// Change:
scanErrors = append(scanErrors, fmt.Sprintf(&quot;%s: %v&quot;, img.Image, err))
// To:
scanErrors = appe</details>

<details><summary>pkg/cli/docker_images.go:142</summary>

**`GrypeImage = &quot;anchore/grype:latest&quot;` is unpinned, unlike every other tool image**: this makes scan results non-reproducible across runs as the grype database and binary silently update, and breaks the consistency guarantee users expect from a security scanner.

&lt;details&gt;
&lt;summary&gt;💡 Details&lt;/summary&gt;

Compare to the other tool images:
- `ActionlintImage = &quot;rhysd/actionlint:1.7.12&quot;`pinned
- `RunnerGuardImage = &quot;ghcr.io/vigilant-llc/runner-guard:latest&quot;`also unpinned (pre-existing issue)…

</details>

@github-actions

Copy link
Copy Markdown
Contributor

Great work on the grype integration! 🔍 This PR adds container image vulnerability scanning to the compile pipeline with excellent coverage:

Comprehensive implementation — The grype feature integrates cleanly with the existing compile pipeline (--grype alongside --actionlint, --poutine, etc.), with proper deduplication of image refs and severity mapping.

Well-tested — 15 new unit tests cover cache behavior, image collection, deduplication, and edge cases. Tests for the Docker integration updated to match the new signature.

Clear documentation — The PR body explains the design (image extraction, Docker runs, severity mapping) and provides usage examples.

Focused change — All modifications serve the single goal of adding grype scanning. No unrelated changes.

This looks ready for review!

Generated by ✅ Contribution Check · sonnet46 73.6 AIC · ⌖ 7.67 AIC · ⊞ 6.2K ·

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@copilot update static analysis agentic workflow

Updated in commit 4682fa2: static-analysis agentic workflow now includes Grype in image pre-pull/verification, compile runs with --grype, and the reporting prompt/template text has been updated to include all five tools.

@pelikhan

Copy link
Copy Markdown
Collaborator

@copilot run pr-finisher skill

@pelikhan

Copy link
Copy Markdown
Collaborator

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>

Copilot AI commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@copilot run pr-finisher skill

Ran a pr-finisher pass and pushed 1524f64 to address the current failing check root cause (lint-go in pkg/cli/grype_test.go). Local validation passed (make fmt, make lint, TEST_UNIT_RUN_FULL=1 make test-unit, make test, make recompile, make agent-report-progress).

Copilot AI commented Jul 23, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Fix with Copilot: https://github.com/github/gh-aw/actions/runs/29984013358/job/89132012637

Fixed by commit 1524f64. The linked run failed in pkg/cli/grype_test.go (errorlint/perfsprint), and this commit updates the test assertions/error construction to satisfy lint-go. A newer CGO run for this branch (29984781067) is successful.

@pelikhan
pelikhan merged commit c301144 into main Jul 23, 2026
25 checks passed
@pelikhan
pelikhan deleted the copilot/add-container-image-validation-support branch July 23, 2026 06:32
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This pull request is included in a new release.

Release: v0.83.1

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.

3 participants