Skip to content

feat(security): assess source, image and cluster on a schedule - #30

Merged
Tiago0507 merged 4 commits into
mainfrom
feat/continuous-security
Sep 21, 2026
Merged

Tiago0507 merged 4 commits into
mainfrom
feat/continuous-security

Conversation

@Tiago0507

Copy link
Copy Markdown
Contributor

What changes

A scheduled vulnerability assessment that re-checks artifacts which were
already published. .github/workflows/continuous-security.yml is a reusable
workflow; tests/workflows/continuous-security.bats is its contract and
continuous-security-self-test.yml runs that contract on every change that
could break it.

The schedule lives in each caller, never here. GitHub does not fire schedule
for a reusable workflow, so a cron in this file would silently never run.

Three surfaces, each recorded as PASS, FAIL or BLOCKED, the vocabulary the
observability lane's collectors already use:

  • source: the caller's own checkout, scanned with the pinned Trivy action.
    Needs no credential, so it works from the first scheduled run.
  • image: the digests the GitOps repository declares for that service, which
    is what "running artifact" means here. Read through a read-only App token
    scoped to that one repository, the same pattern mirror-platform-images.yml
    uses for the locked image set, then scanned with the checksum-locked Trivy
    release the toolchain lock records.
  • cluster: the lane's existing read-only verify-security.sh, rather than a
    second imperative path into a cluster.

A surface without credentials is BLOCKED and says why. It is never counted as a
pass, and it never fails the run as though the platform were broken.

Findings route into one issue in the repository that owns the artifact. A
recurring finding comments on the open issue instead of filing a new one, and a
clean run closes it.

Why

FR-035 requires that running artifacts and their host environments be assessed
continuously for newly disclosed vulnerabilities, with actionable findings
routed into the review process.

Nothing did that. ci.yml scans what a push builds, so a repository nobody
touches is never re-checked; a CVE disclosed against an image built last month
is invisible. Trivy Operator scans what a cluster runs and reports to Slack and
a dashboard, but it never looks at source and it opens nothing anyone is
assigned. Between them there was no periodic reassessment and no owner.

Tasks

gitops specs/009-full-platform-rollout T143 (test, delivered here)
gitops specs/009-full-platform-rollout T146 (workflow, partial: callers pending)

  • The task register is updated in this pull request
  • Every task ticked here was verified by locating its artifact

The register lives in microservice-app-gitops, not in this repository, and the
suite's convention is that a task whose artifact lands elsewhere is ticked in a
separate GitOps pull request after this one merges. No task is ticked here.

T146 is partial. It also requires wiring the five service repositories and
the ops and GitOps callers. A caller must pin this workflow by an immutable
commit that exists on main, which cannot be known until this pull request
merges, so the seven caller pull requests follow immediately afterwards and the
GitOps register records T143 and T146 together once they are all in.

How it is verified

The contract fails before the implementation and passes after.
test(security): specify the continuous security assessment was committed red:

continuous-security-contract: workflow is missing: .github/workflows/continuous-security.yml

After the implementation:

$ bash tests/workflows/continuous-security.bats
continuous-security-contract: OK: reusable and caller-scheduled, source/image/cluster
surfaces with explicit PASS/FAIL/BLOCKED, digests read from GitOps through a read-only
App token, findings routed to one deduplicated issue in the owning repository, OIDC with
no static credentials, SHA-pinned actions, and no cluster mutation.

Two corrections to the contract, each in its own commit, both found by
running it against a real implementation rather than against nothing:

  • It forbade the bare string schedule: and so reported the header comment that
    explains why the schedule belongs in the caller. It now matches a YAML key at
    the start of a line.
  • It required scan-type: image, which assumes the Trivy action. The set of
    declared digests is resolved at run time and an action cannot iterate it, so
    the implementation installs the checksum-locked Trivy release and the contract
    now asserts that release, its checksum, and the verified install.
  • It also required a vars.AWS_ACCOUNT_ID reference. Taking the registry as an
    input, the shape ci.yml already uses for ecr-repository, is stronger: the
    workflow then holds no account at all, and the contract now forbids any
    twelve-digit registry literal.

Every other suite in this repository still passes:

tests/github-actions-runtime.sh                  PASS
tests/ci-contract.sh                             PASS
tests/conventions-contract.sh                    PASS
tests/workflows/reusable-workflow-contract.bats  PASS
tests/workflows/mirror-platform-images.bats      PASS
tests/workflows/continuous-security.bats         PASS

Both workflow files parse as YAML and declare the intended triggers, checked
with PyYAML in a pinned container:

.github/workflows/continuous-security.yml OK; triggers: ['workflow_dispatch', 'workflow_call']
   job assess steps: 15
.github/workflows/continuous-security-self-test.yml OK; triggers: ['pull_request', 'push']
   job contract steps: 2

One Actions semantic was removed rather than assumed. An earlier draft read
secrets.gitops-reader-app-id inside a step if. The secrets context is not
available there, so whether the read-only credentials were wired is now resolved
in a step, where the context is available, and read back as a step output.

No digest or checksum was invented. The Trivy release and its checksum are
the ones microservice-app-gitops/scripts/managed/full-profile-toolchain.lock
records for v0.74.0; every action SHA is one this repository already pins.

Documentation MCP servers. ../microservice-app-ai-agents/scripts/check-mcp.sh gitops .
reports PASS aws-knowledge answers with 5 tools, PASS microsoft-learn answers with 3 tools. This pull request adds no infrastructure manifest and no
provider argument: it is a GitHub Actions workflow and a bash contract. The one
external behavior it depends on, that schedule does not fire for a reusable
workflow and must be declared by the caller, is GitHub Actions behavior that
neither server documents; it is recorded in the workflow header so the next
reader does not have to rediscover it.

  • Infrastructure changes: the documentation consulted through the required MCP servers is listed above

Risk and rollback

Nothing runs until a caller schedules it, and no caller exists yet, so merging
this changes no behavior in any repository.

When callers land, the risks are bounded and named:

  • The workflow needs issues: write in the caller to route a finding. That is
    the only write permission it asks for; it has no contents: write.
  • A failing assessment fails the scheduled run and opens or updates one issue.
    It cannot block a pull request, because no caller runs it on pull_request.
  • The image and cluster surfaces are inert until their infrastructure exists,
    the same authored-but-inactive state mirror-platform-images.yml has carried
    since it was written. They report BLOCKED rather than failing.
  • The cluster surface shells out to the GitOps repository's own read-only
    collector. The contract rejects any mutating kubectl verb in this file.

Rollback is git revert of the merge. Callers pin an immutable commit, so a
revert cannot change what an already-merged caller runs.

What this PR does not do

  • No callers. The five service repositories and the ops and GitOps callers
    follow in their own pull requests, once there is a merge commit on main to
    pin. Until then T146 stays partial and unticked.
  • No register update. T143 and T146 are ticked in microservice-app-gitops
    after the callers merge, in a separate pull request, as the suite's task
    convention requires.
  • No live evidence. No cluster carries Argo CD and no reader role exists, so
    the image and cluster surfaces have never run. They are contract-tested, not
    observed, and the workflow reports that as BLOCKED rather than as a pass.
  • No change to ci.yml, release.yml or promote.yml. This is an addition
    beside them, not a modification of the delivery path.
  • No new secret or variable is created. The App credentials it can consume
    are the ones mirror-platform-images.yml already defines.

Merge method: merge commit. This pull request carries an SDD pair
(test(security) committed red, then feat(security)), and
microservice-app-docs/docs/Pull request and task tracking conventions.md #40
requires the pair to survive.

Approval required: one approval from EstebanGZam or Juanmadiaz45. The
author does not merge this himself, and no AI agent approves it.

🤖 Generated with Claude Code

Adds tests/workflows/continuous-security.bats, the contract for the scheduled
vulnerability assessment gitops spec 009 T146 must provide and T143 must test.
It follows tests/workflows/mirror-platform-images.bats: grep the guarantees the
task requires, and equally the things it forbids.

The contract requires a reusable workflow that its callers schedule, because
GitHub never fires schedule for a reusable workflow and a cron here would
silently never run; the three surfaces T146 names, with the image surface
reading the digests the GitOps repository declares through a read-only App
token as mirror-platform-images.yml already does and the cluster surface
reusing the lane's existing read-only verify-security.sh; an explicit PASS,
FAIL or BLOCKED per surface, so a surface without credentials is never counted
as a pass; routing into one deduplicated issue in the repository that owns the
artifact, updated rather than reopened when a finding recurs and closed when it
clears; OIDC with no static credentials; the declared account rather than a
hard-coded one; no cluster-mutating verb; and every action pinned by full SHA.
It also requires the self-test that runs this contract on every change that
could break it.

It fails as required: .github/workflows/continuous-security.yml is missing.
…ot as text

Three corrections to the contract added in the previous commit, all found by
running it against a real implementation.

The schedule check forbade the bare word "schedule:", so it reported the header
comment that explains why the schedule belongs in the caller. It now matches a
YAML key at the start of a line, which is the thing it meant to forbid.

The image surface asserted "scan-type: image", which assumes the Trivy action.
The set of declared digests is resolved at run time and an action cannot
iterate it, so the implementation installs the checksum-locked Trivy release
the toolchain lock records, the way mirror-platform-images.yml installs its
tooling. The contract now asserts that release, its checksum, and the verified
install instead.

The account check required a vars.AWS_ACCOUNT_ID reference. Taking the registry
as an input, the shape ci.yml already uses for ecr-repository, is stronger: the
workflow then holds no account at all. The contract now requires that input and
forbids any twelve-digit registry literal.

The contract still fails for the right reason:
.github/workflows/continuous-security.yml is missing.
Makes tests/workflows/continuous-security.bats pass (gitops spec 009 T146,
FR-035). Trivy in ci.yml scans what a push builds and Trivy Operator scans what
a cluster runs; neither re-assesses an artifact that was already published when
a vulnerability is disclosed days later. This closes that gap.

The schedule lives in each caller, never here: GitHub does not fire schedule
for a reusable workflow, so a cron in this file would silently never run.

Three surfaces, each recorded as PASS, FAIL or BLOCKED, the vocabulary the
lane's collectors already use. Source scans the caller's own checkout and needs
no credential, so it works from the first run. Image resolves the digests the
GitOps repository declares for the service, through a read-only App token
scoped to that one repository, and scans each distinct one with the
checksum-locked Trivy release the toolchain lock records. Cluster runs the
lane's existing read-only verify-security.sh rather than opening a second path
into a cluster. A surface with no credentials is BLOCKED and says why; it is
never counted as a pass and never fails the run as though the platform were
broken.

Findings route into one issue in the repository that owns the artifact. A
recurring finding comments on the open issue instead of filing a new one,
because a daily schedule that opened an issue per run would bury the signal in
its own noise, and a clean run closes it.

The registry is an input, the shape ci.yml already uses, so no account literal
lives in this file. Like mirror-platform-images.yml, the parts that need
infrastructure nobody has built yet are authored and contract-tested but
inactive: the image surface needs the reader role, and the cluster surface
needs a cluster with Argo CD (T063-T065).
The suite writes no em dash. Replaces the three the previous two commits
introduced, in the contract's summary line and the self-test header, with plain
punctuation. No assertion and no behavior changes; the contract still passes.
@Tiago0507
Tiago0507 merged commit 7602337 into main Sep 21, 2026
2 checks passed
@Tiago0507
Tiago0507 deleted the feat/continuous-security branch September 21, 2026 01:01
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.

1 participant