PlatformEngr® DevSecOps pipeline course

Module 02 · Source and pipeline · 60 min · You are: Platform team

Workflows and artifacts

In this module

  • You learn: Follow untrusted text and artifacts across low and high privilege jobs.
  • You do: Test issue-title interpolation and the workflow_run report consumer, restrict permissions, treat artifacts as JSON data, and audit workflows with zizmor.
  • You end with: S02 is covered, jobs start read-only, and the privileged report job never runs downloaded files.

A green CI run can still leave attacker-controlled artifacts for a later privileged workflow. Every consumer must treat that output as untrusted data.

Open this module in the interactive course to track progress.

Change journey practised: Surface → Test → Measure → Prove

Concepts

Surface: workflow inputs and hand-offs
The Actions cache is shared by runs on the same branch scope; untrusted pull request caches are scoped to their merge ref.
Trust boundary
A read-only token does not make an artifact trustworthy.
Evidence
Break it: a main-branch cache carries an unreviewed marker

Files to know

Open them in your editor or on GitHub; the bullets say what to look at.

Steps

1. Test: an issue title reaches a workflow

Open only this repository issue. The seeded expression inserts the title into a shell command. Use a harmless marker and check the log for LAB-INJECTION-PROOF. Do not attempt file access or network calls.

gh issue create --repo YOUR_GITHUB_USER/devsecops-lab --title 'Cart"; printf "LAB-INJECTION-PROOF"; echo "' --body 'Local course test'

The marker in the run log proves the title was parsed as shell syntax, not ordinary text.

gh run list --repo YOUR_GITHUB_USER/devsecops-lab --workflow triage.yml --limit 3

2. Measure and prove: workflows pass the audit

Move every untrusted event value through an environment variable, set job permissions explicitly, remove execution of downloaded artifacts, run zizmor, and update S02 with evidence.

cd devsecops-lab && zizmor .github/workflows/

Review each finding by rule and line. Medium and high findings need a code change or a documented exception.

cd devsecops-lab && gh run list --workflow ci.yml --limit 3

3. Test: inspect the platform defaults

Check the token permission default and cache behavior in your own public repo. Record platform protections separately from controls your team wrote.

gh api repos/YOUR_GITHUB_USER/devsecops-lab/actions/permissions/workflow

Simplified for the workshop

The reference repository is a local learning fixture. Canaries, package publishing and service endpoints are disposable and do not grant access to a real account.

The checks read repository files as data. They do not run learner scripts or install dependencies.

Break it on purpose: Evidence is a repeatable test result tied to an owner, commit and retest.

A teammate makes a small change to save time. Work out which trust assumption it changes before you run the test.

You practise: You practise tracing the cheapest path from an input to a capability and identifying an independent control.

1. Break it

cd devsecops-lab && git diff -- .github/ docs/

2. What you see

Expected: the proof is stopped at the intended boundary. You get a successful path or missing evidence. Read the workflow log and the register entry.

3. Fix it yourself

Restore the intended boundary and rerun the same test. Done when the unsafe path is refused, the evidence is recorded and the files pass the repository validator.

Hint 1

Name who controls the input and what privilege consumes it.

Hint 2

Follow the input to the first capability and remove that trust or privilege.

The fix

Use the test and the exact policy file named above; do not add a blanket permission.

cd devsecops-lab && ./scripts/validate.sh
After you fixed it: the five questions
What failed?
A trust boundary allowed untrusted input to reach a useful capability.
How was it detected?
The predicted result and observed log did not match; preserve the commit and workflow run as evidence.
Who responds?
The owner listed for this surface in docs/attack-surface.yaml.
How is it corrected?
Close the specific boundary and repeat the original test.
How is it prevented?
Keep a parse-only lab check and a review rule on the owning files.

Quiz

  1. Which question best identifies the cheapest path?
    • Which accepted input reaches the most useful capability with the fewest prerequisites?
    • Which scanner has the most findings?
    • Which team owns the largest number of files?
  2. What does a passing signature prove?
    • The named identity signed these bytes.
    • The source code is harmless.
    • The image has no known vulnerabilities.
  3. When is a surface covered?
    • The same bounded test is refused and evidence names the control and owner.
    • A scanner has no critical findings.
    • A policy file exists in the repository.
Answers
  1. Which accepted input reaches the most useful capability with the fewest prerequisites? Attack paths follow trust and capability, not tool counts.
  2. The named identity signed these bytes. Signatures bind identity and bytes, not safety.
  3. The same bounded test is refused and evidence names the control and owner. Presence is not proof; retest the attack path.

Field note

I ask learners to write the expected signal before they run a test. It makes a control failure easier to tell from a test that never reached the boundary.

Report a problem with this module