Repair a security pipeline for GitHub Actions and GitLab CI
Node.js 22 or newer for the checks. No dependencies, account or network access.

Reference solution: not included here, so the exercise does not hand you the answer.
Attempt the repair first. Then download security-pipeline-solution.zip from the project card
on https://softwaresecurity.com/labs/#project-security-pipeline and extract it into this folder;
it adds reference.github.yml and reference.gitlab-ci.yml without changing your files.

Outcome
Two pipelines that run secrets scanning (gitleaks), SAST (Semgrep CE), SCA (OSV-Scanner),
an image scan and an SBOM (Trivy), and keyless signing of the image digest (cosign), with
gates that can actually fail and a CI identity that cannot be borrowed by a pull request.
The repaired files are ready to copy into a repository that has a Dockerfile at its root.

Files
exercise.github.yml       starter GitHub Actions workflow (repair this)
exercise.gitlab-ci.yml    starter GitLab CI pipeline (repair this)
exercise.test.mjs         13 static checks of both files
pipeline-checks.mjs       the line-based helpers the checks use

1. See what is wrong
   node --test exercise.test.mjs
   Expect 1 passing and 12 failing tests. Each failure names the line or job to fix.
   Every scanner already runs, so the pipeline looks finished; the failures are about
   what a scan result can change and what an attacker can reach.

2. Repair the GitHub workflow
   - Trigger on pull_request, never pull_request_target, so fork code never runs with
     secrets or a write token.
   - Set permissions: {} at the top and give each job only what it needs. Only the
     release job gets packages: write and id-token: write.
   - Pin every action to a full 40-character commit SHA with a "# vX.Y.Z" comment, and
     every image to a digest. Resolve them yourself:
       gh api repos/actions/checkout/git/ref/tags/v7.0.1 --jq .object.sha
       docker buildx imagetools inspect aquasec/trivy:0.75.0 --format '{{.Manifest.Digest}}'
     An annotated tag points to a tag object; dereference it with
       gh api repos/OWNER/REPO/git/tags/TAG_OBJECT_SHA --jq .object.sha
   - Set persist-credentials: false on every checkout, and drop the ref: override that
     checks out the pull request head; with pull_request the default checkout is the change.
   - Pass event data to scripts through env:, never ${{ ... }} inside run:.
   - Make gates fail: no continue-on-error, no "|| true", Trivy with --exit-code 1,
     Semgrep with --error, gitleaks in git mode over full history (fetch-depth: 0).
   - Release only on pushes to main, after every gate, and sign and attest the pushed
     digest, never a tag.

3. Repair the GitLab pipeline
   - Pin every image and service by digest.
   - Remove allow_failure and "|| true" from scan jobs; Trivy with --exit-code 1.
   - Sign only on the default branch, after every scan job, keylessly with an
     id_tokens: SIGSTORE_ID_TOKEN (aud: sigstore) token, and sign "$CI_REGISTRY_IMAGE@$IMAGE_DIGEST".
     Write the digest to a dotenv artifact in the build job so later jobs can use it.

4. Check your work
   node --test exercise.test.mjs
   With the solution extracted:
   EXERCISE_MODULE=reference node --test exercise.test.mjs
   The reference GitHub workflow also passes actionlint 1.7.12 and zizmor 1.30.1
   (offline, auditor persona). Run them on yours:
   mkdir -p check/.github/workflows && cp exercise.github.yml check/.github/workflows/security.yml
   (cd check && git init -q && docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:1.7.12)
   (cd check && docker run --rm -v "$PWD:/repo" ghcr.io/zizmorcore/zizmor:1.30.1 --offline /repo/.github/workflows/security.yml)
   zizmor finds the trust problems in the starter but not the gates that cannot fail;
   the tests here check both.

5. Use it in a repository
   Copy the repaired GitHub file to .github/workflows/security.yml, or the GitLab file
   to .gitlab-ci.yml. Add osv-scanner.toml for any time-boxed exceptions:
     [[IgnoredVulns]]
     id = "GHSA-xxxx-xxxx-xxxx"
     ignoreUntil = 2026-12-31
     reason = "Not reachable: the parser is never given untrusted input (ticket SEC-123)"
   Verify a signed image before deploying it, with the identity of the workflow that signed it:
     cosign verify ghcr.io/OWNER/REPO@sha256:DIGEST \
       --certificate-identity "https://github.com/OWNER/REPO/.github/workflows/security.yml@refs/heads/main" \
       --certificate-oidc-issuer https://token.actions.githubusercontent.com
     cosign verify registry.gitlab.com/GROUP/PROJECT@sha256:DIGEST \
       --certificate-identity "https://gitlab.com/GROUP/PROJECT//.gitlab-ci.yml@refs/heads/main" \
       --certificate-oidc-issuer https://gitlab.com
   Uploading SARIF from a private GitHub repository needs GitHub Code Security; the
   GitLab SAST report widget needs GitLab Ultimate, but the report artifact is kept on
   every tier. Keyless signing with the public Sigstore instance works from GitLab.com,
   not from self-managed GitLab.

Limits
The checks read the YAML as text and test the properties listed above; they do not run
the pipelines. Each scan command was run locally against a seeded repository with the
pinned images on 2026-10-10, and the reference GitHub workflow passed actionlint and
zizmor. Neither file has been run on GitHub or GitLab infrastructure from this project.
Pins age: update them through reviewed pull requests, for example with Dependabot or
Renovate, and re-run the checks.

Dev container
.devcontainer/devcontainer.json opens this folder in VS Code Dev Containers or
GitHub Codespaces using the Dev Containers Node.js 24 image, pinned by digest.
Run the node commands above in its terminal. The actionlint and zizmor commands in
step 4 use Docker, which the image does not include; run those on your host.
