Reviewed and updated: 22 June 2026. Tool versions and documentation were checked against their official projects on that date.
My practical definition of DevSecOps is simple: give developers trustworthy security feedback while they can still act on it.
A credential found before merge is a manageable interruption. The same credential discovered after misuse is an incident.
Pipelines can fail in both directions. Some teams perform almost no automated security testing until a release review. Others add so many noisy scanners that developers learn to ignore the red crosses.
Neither approach creates an effective security programme. One identifies risk too late; the other creates warning fatigue.
The rollout below is designed for a small engineering team using GitHub Actions. It starts with a few checks that are understandable, reasonably fast and assigned to named owners. It does not assume that installing more scanners automatically improves security.
DevSecOps in one table
| Check | When to run it | Initial policy | Primary owner |
|---|---|---|---|
| Secret detection | Every pull request and push | Block high-confidence findings | Repository team |
| Static analysis | Every pull request | Start advisory; later block a small reviewed ruleset | Repository team |
| Dependency scanning | Pull requests and scheduled scans | Advisory first; block policy-defined high-risk findings | Repository team |
| Container scanning | After building the candidate image | Block relevant, fixable critical findings | Application and platform teams |
| SBOM generation | For release artifacts | Generate and retain with the release | Release or platform team |
| Kubernetes manifest scanning | Before deployment | Block explicit unsafe configurations | Platform team |
| DAST | Against staging or a preview environment | Advisory until scope and authentication are reliable | Application security and repository team |
Scan the thing you will actually ship
A source-tree scan and a release-artifact scan answer different questions.
Static analysis looks for risky patterns in source code. Dependency scanning examines declared or resolved software dependencies. A container scan reports packages and known vulnerabilities found inside an image. An SBOM records the components associated with an artifact. Manifest scanning checks the runtime configuration encoded in Kubernetes resources.
These should not be collapsed into one vaguely named security-scan job.
Each check needs:
- a clear purpose;
- an owner;
- an expected response;
- a documented exception process;
- a defined condition for blocking a merge or release.
When a job fails, the developer should be able to determine what failed, why it matters and what action is expected.
Pin the security pipeline itself
Security scanners execute with access to source code, workflow data and, in some jobs, credentials. The scanners therefore form part of the software supply chain.
GitHub recommends referencing actions by full commit SHA because that is the only immutable action reference. This is particularly important after the March 2026 Trivy incident, during which numerous action tags were moved to malicious commits.
The workflow below uses full commit SHAs for GitHub Actions. The comments retain the corresponding release versions so Dependabot can propose reviewed updates. GitHub supports updating actions referenced by SHA when the version comment appears on the same line.
A baseline GitHub Actions workflow
This baseline performs three jobs:
- makes complete Git history available to Gitleaks;
- runs Semgrep in advisory mode;
- builds a candidate image and scans that exact local image with Trivy.
name: devsecops-baseline
on:
pull_request:
push:
branches:
- main
workflow_dispatch:
permissions:
contents: read
concurrency:
group: devsecops-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
secret-scan:
name: secret-scan
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Check out the repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
fetch-depth: 0
persist-credentials: false
- name: Scan for committed secrets
uses: gitleaks/gitleaks-action@e0c47f4f8be36e29cdc102c57e68cb5cbf0e8d1e # v3.0.0
env:
GITHUB_TOKEN: ${{ github.token }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
GITLEAKS_ENABLE_COMMENTS: "false"
sast-advisory:
name: sast-advisory
runs-on: ubuntu-24.04
timeout-minutes: 15
container:
image: semgrep/semgrep:1.167.0
steps:
- name: Check out the repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run advisory Semgrep scan
run: semgrep scan --config auto
container-scan:
name: container-scan
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- name: Check out the repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Build the candidate image
run: docker build --tag app:${{ github.sha }} .
- name: Record the local image identifier
run: |
docker image inspect \
--format 'Scanned local image ID: {{.Id}}' \
app:${{ github.sha }}
- name: Scan the candidate image
uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0
with:
image-ref: app:${{ github.sha }}
format: table
exit-code: "1"
ignore-unfixed: true
severity: CRITICAL
scanners: vulnThe action SHAs above correspond to the stated releases.
The Semgrep image is pinned to a numbered version for readability. Before adopting the workflow in a controlled production environment, resolve and record the reviewed OCI image digest for the runner architecture rather than relying only on a mutable image tag.
Gitleaks organisation and fork considerations
GITLEAKS_LICENSE is required for organisation-owned repositories using Gitleaks Action. It is not required for a personal repository.
GitHub does not expose ordinary repository secrets to pull requests from forks. Public organisation repositories that accept external contributions therefore need a separate Gitleaks CLI or container scan that does not require a secret. Do not solve this by using pull_request_target to check out and execute untrusted pull-request code.
Make failed checks enforceable
A failed GitHub Actions job does not by itself prevent a merge.
Create a repository ruleset or branch-protection rule for main and require the checks intended to act as gates, for example:
secret-scan
container-scanLeave sast-advisory visible but not required while the rules are being evaluated.
GitHub prevents a merge only when the configured required status checks have completed successfully. Job names should also remain unique across workflows to avoid ambiguous required checks.
Move Semgrep from advisory to blocking deliberately
The baseline command is advisory:
semgrep scan --config autoIt reports findings but does not fail solely because findings were detected. Scan errors still fail the command.
Use this phase to determine:
- which languages and frameworks need coverage;
- which rules identify genuine defects;
- which generated or vendored directories should be excluded;
- which findings have a clear remediation path.
Once the team trusts a small ruleset, store it in the repository and use it for the blocking job:
semgrep scan --config .semgrep/blocking.yml --errorThe checked-in ruleset should be reviewed like application code. Avoid turning the changing results of --config auto directly into a mandatory gate. Semgrep distinguishes registry-selected automatic rules from explicit policy configurations, and --error is what makes detected findings produce exit code 1.
Understand what the image job proves
The example scans the exact local image built earlier in the same job. That is useful for pull-request feedback, but it is not yet release provenance.
A production release workflow should:
- build the image once;
- push that image to the registry;
- obtain its immutable registry digest;
- scan or verify that digest;
- generate the SBOM from the same artifact;
- deploy by digest;
- avoid rebuilding the image between scanning and deployment.
A tag such as:
app:<git-commit>helps trace an image back to source, but a tag remains a name. The registry digest identifies the image content.
Treat ignore-unfixed as a policy choice
This example begins with:
ignore-unfixed: true
severity: CRITICALThat can reduce early noise, but it must not be interpreted as “all remaining risk is acceptable.”
Trivy’s ignore-unfixed option also suppresses findings classified as will_not_fix, fix_deferred and end_of_life, in addition to currently affected findings.
Compensating controls should include:
- detecting end-of-life base images separately;
- recording suppressed findings;
- allowing emergency review of severe unfixed vulnerabilities;
- adding expiry dates and reasons to approved exceptions;
- periodically reconsidering the initial severity threshold.
A remotely exploitable critical vulnerability without a vendor patch may still justify delaying a release. The scanner supplies evidence; it does not make the risk decision.
What to block first
Begin with findings that have high confidence, meaningful impact and an obvious response.
High-confidence secret findings
Treat a credible committed credential as exposed. The normal response is to:
- revoke or disable it;
- rotate affected credentials;
- inspect relevant audit logs;
- remove it from history where appropriate;
- correct the process that allowed it to be committed.
Deleting the string from the newest commit is not remediation.
Relevant, fixable critical image vulnerabilities
Confirm that the affected package exists in the runtime image, that the vendor provides a fix and that the finding is applicable to the deployed environment.
Do not use severity alone as a substitute for context.
Explicitly unsafe Kubernetes settings
Good initial blocking controls include unapproved:
- privileged containers;
- host namespace access;
- dangerous Linux capabilities;
- writable host filesystem mounts;
- workloads that violate the organisation’s non-root policy.
Prefer explicit rules over a broad requirement such as “every Kubesec score must be positive.”
A reviewed SAST ruleset
Block only rules that the team has evaluated against its own languages and frameworks. Each blocking rule should have a clear explanation, owner and exception process.
A 30-day rollout
Week 1: secret detection and incident response
Add Gitleaks locally and in CI.
Before making the check mandatory, document the response to a credible finding:
- who revokes the credential;
- how it is rotated;
- who reviews audit logs;
- when history rewriting is appropriate;
- how affected deployments are identified;
- how false positives are suppressed and reviewed.
Configure secret-scan as a required status check after testing the workflow against representative repositories.
Week 2: candidate image scanning and artifact identity
Build the candidate image and scan it with Trivy.
Review every critical finding before widening the policy. Record the local image identifier in pull-request jobs. In the release workflow, publish once and retain the registry digest so the scanned artifact can be connected to the deployed artifact.
Generate an SBOM from that same release image rather than rebuilding it separately.
Week 3: tune static and dependency analysis
Run Semgrep in advisory mode.
Identify the rules that consistently find real defects and promote only those rules into a checked-in blocking configuration.
Add dependency analysis separately. Container scanning and source dependency scanning overlap, but they do not inspect exactly the same inputs or lifecycle stages.
Week 4: rendered manifests and staging tests
Scan the Kubernetes resources that will actually be deployed.
For Helm, render the selected values before scanning:
helm template -f values.yaml ./chart | kubesec scan /dev/stdinKubesec supports named-rule scans and warns against submitting sensitive manifests to its public service, so sensitive deployment configuration should be scanned locally.
Add a lightweight DAST job against staging or a short-lived preview environment. Keep it advisory until authentication, test data, destructive actions and scan scope have been configured reliably.
DAST does not automatically belong in the critical path of every pull request. Stable smoke scans may fit preview environments; broader authenticated scans may be better suited to scheduled staging runs.
Keep pinned actions current
Commit a Dependabot configuration for GitHub Actions:
version: 2
updates:
- package-ecosystem: github-actions
directory: /
schedule:
interval: weeklyDependabot can propose updates for actions referenced by full commit SHA. The version comment beside each SHA keeps the reviewed release visible to maintainers.
Review the proposed release, source changes and commit before merging the update. Pinning prevents an existing workflow reference from moving; it does not establish that every future update is trustworthy.
Container-image dependencies, including the Semgrep image, need a separate digest-update process.
Measure whether the rollout is working
Scanner totals are a poor success metric. A growing number of findings may mean risk is increasing, but it may also mean coverage improved.
More useful measures include:
- median time to resolve confirmed findings;
- false-positive rate by rule;
- repeated classes of defect;
- findings discovered before merge rather than after release;
- exceptions past their expiry date;
- percentage of releases deployed from the scanned digest;
- percentage of repositories using required checks;
- time taken to revoke a leaked credential;
- scanner failures that prevent results from being produced.
The important question is not:
How many security tools are installed?
It is:
Did the engineer receive a trustworthy signal early enough to change the outcome?
Frequently asked questions
Is DevSecOps just DevOps plus scanners?
No. Scanners provide evidence. DevSecOps also requires ownership, response procedures, secure defaults, enforceable policy, exception management and feedback that developers trust.
Should every finding fail a build?
No. A finding should block only when it is sufficiently reliable, significant and actionable under the organisation’s policy.
Other findings should remain visible while rules are tuned and a baseline is established.
Does a red GitHub Actions job prevent merging?
Not necessarily. The job must be configured as a required status check through a repository ruleset or branch-protection rule.
Is a Gitleaks finding a verified live credential?
No. It is a secret detection. Validate the context, but treat a credible committed credential as exposed until it has been revoked or shown to be harmless.
Do I need DAST on every pull request?
Not always. DAST requires a running target, predictable test data, controlled scope and often authentication. Staging or a preview environment is normally the appropriate target; the right frequency depends on scan duration and reliability.
Does scanning an image tag prove that the deployed image was scanned?
No. To establish that connection, publish the image once, record its registry digest and deploy that same digest without rebuilding it.


Leave a Reply