Skip to content
Draft
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
56 changes: 48 additions & 8 deletions .github/workflows/bakery-build-native.yml
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,7 @@ jobs:
contents: read
packages: write
id-token: write
security-events: write
needs: matrix
# GitHub Actions fails (not skips) a matrix job when the matrix evaluates to [].
# Guard here so an empty change-aware matrix (a push with nothing to build)
Expand Down Expand Up @@ -456,8 +457,17 @@ jobs:
# name-free `repo@sha256:...` entry per target -- all of them coexist,
# unlike tags. wizcli's `extract` driver reads the local store, so it can
# only resolve every target once Test has warmed it.
- name: Scan
if: ${{ inputs.push && steps.filter-steps.outputs.wiz-auth == 'true' }}
# Production builds scan only the latest version of each image, now that the
# backlog has been scanned once; development builds keep scanning every push.
# Gated here rather than by passing `--latest` to the scan: this job is already
# pinned to one version, so `--latest` combined with that pin resolved to an
# empty target set on every non-latest version and exited 1, which
# continue-on-error then hid behind a red annotation on an otherwise green job.
# matrix.img.latest comes from `bakery ci matrix`, so the "is this the latest
# version" decision is made once, in one place.
- name: Wiz Scan
if: ${{ inputs.push && steps.filter-steps.outputs.wiz-auth == 'true'
&& (inputs.dev-versions == 'only' || matrix.img.latest) }}
continue-on-error: true
env:
IMAGE_NAME: ${{ matrix.img.image }}
Expand All @@ -479,11 +489,6 @@ jobs:
[[ -n "$WIZ_POLICY_ID" ]] && POLICY_FLAGS=(--policies "$WIZ_POLICY_ID")
PROJECT_FLAGS=()
[[ -n "$WIZ_PROJECT_ID" ]] && PROJECT_FLAGS=(--projects "$WIZ_PROJECT_ID")
# Production builds (dev-versions != only) scan only the --latest
# version of each image now that the backlog has been scanned once.
# Development builds keep scanning every push.
LATEST_FLAGS=()
[[ "$DEV_VERSIONS" != "only" ]] && LATEST_FLAGS=(--latest)
WIZCLI_PATH=${GITHUB_WORKSPACE}/tools/wizcli \
bakery wizcli scan \
--image-name "^${IMAGE_NAME}$" \
Expand All @@ -494,9 +499,44 @@ jobs:
"${DEV_SPEC_FLAGS[@]}" \
"${POLICY_FLAGS[@]}" \
"${PROJECT_FLAGS[@]}" \
"${LATEST_FLAGS[@]}" \
--metadata-file "./${IMAGE_NAME}-${IMAGE_VERSION}-${NORMALIZED_PLATFORM}-metadata.json" \
--context "$CONTEXT"

- name: Setup trivy
if: ${{ inputs.push && matrix.img.latest }}
uses: "posit-dev/images-shared/setup-trivy@main"

# After Test for the same reason the Wiz scan is: only Test's `docker run`
# makes every target addressable locally. Scanning earlier silently pulled
# and scanned the published image instead of the one just built.
- name: Trivy Scan
if: ${{ inputs.push && matrix.img.latest }}
continue-on-error: true
env:
IMAGE_NAME: ${{ matrix.img.image }}
IMAGE_VERSION: ${{ matrix.img.version }}
DEV_VERSIONS: ${{ inputs.dev-versions }}
MATRIX_VERSIONS: ${{ inputs.matrix-versions }}
NORMALIZED_PLATFORM: ${{ steps.normalize-platform.outputs.platform }}
CONTEXT: ${{ inputs.context }}
run: |
bakery trivy scan \
--image-name "^${IMAGE_NAME}$" \
--image-version "$IMAGE_VERSION" \
--image-platform "$NORMALIZED_PLATFORM" \
--dev-versions "$DEV_VERSIONS" \
--matrix-versions "$MATRIX_VERSIONS" \
--metadata-file "./${IMAGE_NAME}-${IMAGE_VERSION}-${NORMALIZED_PLATFORM}-metadata.json" \
--context "$CONTEXT"

# No `category:` on purpose: each file carries its own in
# automationDetails.id, and one category across all of them is rejected.
- name: Upload Trivy SARIF
if: ${{ inputs.push && matrix.img.latest && !cancelled() && hashFiles('results/trivy/**/*.sarif') != '' }}
uses: github/codeql-action/upload-sarif@5595ccaf912efad79be6eef63a5619ff05969be3 # v4.37.6
with:
sarif_file: results/trivy

- name: Upload Metadata
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
Expand Down
5 changes: 5 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ jobs:
- name: Setup wizcli
uses: ./setup-wizcli

- name: Setup trivy
uses: ./setup-trivy

- name: Setup ORAS CLI
uses: oras-project/setup-oras@1d808f7d7f6995cc68b7bf507bfe5c5446e1dc9d # v2.0.1

Expand Down Expand Up @@ -164,6 +167,7 @@ jobs:
contents: read
packages: write
id-token: write
security-events: write

uses: "./.github/workflows/bakery-build-native.yml"
secrets:
Expand All @@ -181,6 +185,7 @@ jobs:
permissions:
contents: read
packages: write
security-events: write
uses: "./.github/workflows/bakery-build-pr.yml"
with:
version: ${{ github.head_ref || github.ref_name }}
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ Run `bakery --help` and `bakery <command> --help` for full command reference.
Commands have aliases: `build`/`b`, `create`/`c`, `run`/`r`, `update`/`u`, `remove`/`rm`.

Key commands for product repos: `bakery build`, `bakery build --plan`, `bakery dgoss run`,
`bakery update files`, `bakery create version`, `bakery ci matrix`.
`bakery trivy scan`, `bakery update files`, `bakery create version`, `bakery ci matrix`.

## Image Templating System

Expand Down
2 changes: 1 addition & 1 deletion posit-bakery/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ Full documentation is available at **[posit-dev.github.io/images-shared](https:/
| [dgoss](https://github.com/goss-org/goss#installation) | `bakery dgoss run` | Test container images for expected content & behavior |
| [hadolint](https://github.com/hadolint/hadolint#install) | `bakery hadolint run` | Lint Dockerfile/Containerfile |
| [openscap](https://static.open-scap.org/) | to be implemented | Scan container images for secure configuration and vulnerabilities |
| [trivy](https://trivy.dev/docs/latest/getting-started/) | to be implemented | Scan container images for vulnerabilities |
| [trivy](https://trivy.dev/docs/latest/getting-started/) | `bakery trivy scan` | Scan container images for vulnerabilities |
| [wizcli](https://www.wiz.io/lp/wiz-cli) | `bakery wizcli scan` | Scan container images for vulnerabilities |

## Installation
Expand Down
6 changes: 3 additions & 3 deletions posit-bakery/docs/architecture.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,7 @@ flowchart TD
results[/"Test & Scan results"/]

trivy[[trivy]]
runTrivy[[bakery trivy run]]
runTrivy[[bakery trivy scan]]
runTrivy --> trivy
image -.-> trivy -.-> results

Expand All @@ -244,8 +244,8 @@ flowchart TD
class trivy,openscap,wizcli external

%% Mark what we are working on and is in flight %%
class inprogress,trivy todo
class openscap,results todo
class inprogress todo
class openscap todo
```

### Publish
Expand Down
27 changes: 27 additions & 0 deletions posit-bakery/docs/configuration.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -666,6 +666,33 @@ images:
- linux/arm64
```

#### TrivyOptions

A TrivyOption configures [Trivy](https://trivy.dev/docs/latest/getting-started/) vulnerability scanning for an image target.

Trivy options can be set in the `options` array of an [Image](#image) or an [ImageVariant](#imagevariant). When both are present, the variant's options are merged over the image's options, with any field not explicitly set on the variant inheriting the image-level value. Scanning runs via `bakery trivy scan`, which writes SARIF results to `results/trivy/`. CLI flags (`--severity`, `--fail-on-severity`, `--disabled-scanners`, `--timeout`) take precedence over these options when both are set.

| Field | Description | Default Value | Example |
|-----------------------------------------|-------------------------------------------------------------------|-----------------|---------------------------|
| `tool`<br/>*"trivy" literal string* | *(Required)* The name of the tool. | | `trivy` |
| `severity`<br/>*string array* | Severities to report (e.g. HIGH, CRITICAL). | Trivy default | `["HIGH", "CRITICAL"]` |
| `failOnSeverity`<br/>*string array* | Severities that fail the scan if found. Unset means never fail. | Never fails | `["CRITICAL"]` |
| `disabledScanners`<br/>*string array* | Scanners to disable (e.g. secret, license, misconfig). | Trivy default | `["secret"]` |
| `timeout`<br/>*string* | Timeout for the scan (e.g. 1h, 10m). | Trivy default | `"10m"` |

##### Example TrivyOptions

```yaml
images:
- name: workbench
options:
- tool: trivy
severity: ["HIGH", "CRITICAL"]
failOnSeverity: ["CRITICAL"]
disabledScanners: ["secret"]
timeout: "10m"
```

### DevBuildSpec

A DevBuildSpec is a typed payload passed to the `--dev-spec` CLI option to configure development (daily) version builds. It supports either a dispatch-pinned version or a branch-targeted discovery build. At least one of `version` or `release_branch` must be set.
Expand Down
3 changes: 2 additions & 1 deletion posit-bakery/docs/index.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ Bakery is a CLI tool that binds together various tools to manage a matrixed buil
| [dgoss](https://github.com/goss-org/goss#installation) | `bakery dgoss run` | Test container images for expected content & behavior |
| [hadolint](https://github.com/hadolint/hadolint#install) | `bakery hadolint run` | Lint Containerfiles for best practices |
| [oras](https://oras.land/) | `bakery imagetools merge` | Merge multi-platform images into an OCI index |
| [trivy](https://trivy.dev/docs/latest/getting-started/) | `bakery trivy scan` | Scan container images for vulnerabilities |
| [wizcli](https://www.wiz.io/) | `bakery wizcli scan` | Scan container images for vulnerabilities |

::: {.callout-note}
Additional tool integrations (trivy, openscap) are planned. See the [architecture diagrams](architecture.qmd) for the full roadmap.
Additional tool integrations (openscap) are planned. See the [architecture diagrams](architecture.qmd) for the full roadmap.
:::

## Installation
Expand Down
6 changes: 6 additions & 0 deletions posit-bakery/posit_bakery/cli/ci.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ class RichHelpPanelEnum(str, Enum):
class BakeryCIMatrixFieldEnum(str, Enum):
VERSION = "version"
DEV = "dev"
LATEST = "latest"
PLATFORM = "platform"


Expand Down Expand Up @@ -265,6 +266,7 @@ def matrix(
"image": "image-name",
"version": "version-name",
"dev": false,
"latest": true,
"platform": "linux/amd64"
}
]
Expand Down Expand Up @@ -357,6 +359,10 @@ def matrix(
entry["version"] = ver.name
if BakeryCIMatrixFieldEnum.DEV not in exclude:
entry["dev"] = ver.isDevelopmentVersion
if BakeryCIMatrixFieldEnum.LATEST not in exclude:
# Same predicate the --latest filter uses, so a workflow gating on
# this field selects exactly what `--latest` would have.
entry["latest"] = ver.is_latest_release
if BakeryCIMatrixFieldEnum.PLATFORM not in exclude:
for platform in ver.supported_platforms:
entry["platform"] = platform
Expand Down
44 changes: 30 additions & 14 deletions posit-bakery/posit_bakery/cli/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,27 +43,43 @@ def exit_if_no_targets(config: "BakeryConfig", settings: "BakerySettings") -> No
detail = f" matching {active}" if active else ""
stderr_console.print(
f"❌ No image targets{detail}. Check the --image-name, --image-version, "
"--image-variant, --image-os, and --image-platform filters along with the "
"--dev-versions/--matrix-versions selection.",
"--image-variant, --image-os, --image-platform, and --latest filters along "
"with the --dev-versions/--matrix-versions selection.",
style="error",
)
raise typer.Exit(code=1)


def _describe_active_filters(settings: "BakerySettings") -> str:
"""Render the set filters as a human-readable ``--flag value`` list."""
"""Render the active filters as a human-readable ``--flag value`` list.

Only lists filters the caller actually set. ``--latest`` is included
because it is the one narrowing policy filter whose default (``False``)
does not narrow, so seeing it here always means the caller asked for it --
and it is frequently the filter that emptied the selection while every
selector matched, which a selector-only message hid entirely.
``--dev-versions``/``--matrix-versions`` stay out: they default to
``exclude``, so echoing them would report flags the caller never passed.
"""
f = settings.filter
parts = [
f"--{name} {value!r}"
for name, value in (
("image-name", f.image_name),
("image-version", f.image_version),
("image-variant", f.image_variant),
("image-os", f.image_os),
("image-platform", f.image_platform),
)
if value
]
parts = []
for name, value in (
("image-name", f.image_name),
("image-version", f.image_version),
("image-variant", f.image_variant),
("image-os", f.image_os),
("image-platform", f.image_platform),
):
if not value:
continue
if isinstance(value, (list, tuple)):
# Render as the flag would be typed rather than as a Python list
# repr, which surfaced as "--image-platform ['linux/arm64']".
parts.extend(f"--{name} {v!r}" for v in value)
else:
parts.append(f"--{name} {value!r}")
if settings.latest:
parts.append("--latest")
return ", ".join(parts)


Expand Down
8 changes: 2 additions & 6 deletions posit-bakery/posit_bakery/config/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -1020,12 +1020,8 @@ def generate_image_targets(self, settings: BakerySettings = BakerySettings()):
f"due to not matching version filter '{settings.filter.image_version}'"
)
continue
if settings.latest and (not version.latest or version.isDevelopmentVersion):
reason = (
"development version ignored by --latest"
if version.isDevelopmentVersion
else "not the latest version (excluded by --latest)"
)
included, reason = version.matches_latest_filter(settings.latest)
if not included:
if version_filter_matched:
log.warning(
f"Version '{version.name}' in image '{image.name}' matches --image-version filter "
Expand Down
34 changes: 34 additions & 0 deletions posit-bakery/posit_bakery/config/image/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,40 @@ def matches_dev_filter(
return False, f"dev channel '{vc_str}' does not match --dev-channel '{dev_channel.value}'"
return True, None

@property
def is_latest_release(self) -> bool:
"""Whether ``--latest`` selects this version.

A development version is never the latest release, even if it carries the
flag: ``--latest`` selects the newest *release*. Every dev-version
constructor currently hardcodes ``latest=False``, so the second clause is
belt-and-braces -- but this predicate is what CI gates key on to decide
what gets security-scanned, and a wrong boolean there fails silently.

Deliberately a plain ``property`` and not a ``computed_field``:
ImageVersion is round-tripped back into bakery.yaml via ``model_dump``
(see ``BakeryConfig.patch_version``), and a computed field would write a
derived key into the user's config file.
"""
return self.latest and not self.isDevelopmentVersion

def matches_latest_filter(self, latest: bool) -> tuple[bool, str | None]:
"""Check whether this version should be included given the ``--latest`` filter.

Shares :pyattr:`is_latest_release` with the ``latest`` field emitted by
``bakery ci matrix``, so a workflow gating on that field and a caller
passing ``--latest`` always select the same versions.

:param latest: Whether the --latest filter is active. When False, every
version is included and the filter is a no-op.
:return: A tuple of (included, reason). If excluded, reason explains why.
"""
if not latest or self.is_latest_release:
return True, None
if self.isDevelopmentVersion:
return False, "development version ignored by --latest"
return False, "not the latest version (excluded by --latest)"

@field_validator("extraRegistries", "overrideRegistries", mode="after")
@classmethod
def deduplicate_registries(
Expand Down
Loading
Loading