Skip to content

πŸ›‘οΈ Sentinel: [security improvement] #55

πŸ›‘οΈ Sentinel: [security improvement]

πŸ›‘οΈ Sentinel: [security improvement] #55

# Central multi-language SAST gate for every ContextualWisdomLab repo.
#
# Fills a governance gap left when the duplicate LOCAL Semgrep workflow was
# removed (xtrmLLMBatchPython) in favour of the central required workflows.
# Semgrep auto-detects the languages present, so this runs everywhere and is a
# no-op on repos with no supported source.
#
# semgrep multi-language SAST -> SARIF uploaded under category "semgrep"
#
# Gating is by the JOB result (high sensitivity: fail on WARNING/ERROR, i.e.
# Medium+), ref-independent, exactly like trivy-fs in security-scan.yml. The
# SARIF is uploaded under a DISTINCT category ("semgrep") and is NOT added to
# the code_scanning ruleset rule, so it does not affect auto-merge. The SARIF
# upload is best-effort (continue-on-error) so a repo that has not enabled code
# scanning still gets the gate without a JOB_STATUS_CONFIGURATION_ERROR.
#
# Engine license: Semgrep OSS CLI is LGPL-2.1 (a containerized CLI invoked in
# CI, not linked) β€” acceptable under the commercial-only OSS policy. Registry
# ruleset p/default is the Semgrep community pack.
name: SAST Semgrep
on:
pull_request:
types: [opened, synchronize, reopened, ready_for_review, closed]
branches: [main, master, develop]
push:
branches: [main, master, develop]
schedule:
- cron: "23 3 * * 1"
repository_dispatch:
types: [sast-semgrep-scan]
concurrency:
group: sast-semgrep-${{ github.event.pull_request.base.repo.full_name || github.repository }}-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true
permissions:
contents: read
jobs:
cancel-closed-pr-runs:
if: github.event.action == 'closed'
runs-on: ubuntu-latest
steps:
- run: echo "PR closed; this run only cancels older runs through workflow concurrency."
semgrep:
name: Semgrep (multi-language SAST)
if: github.event.action != 'closed'
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
actions: read
env:
# Deterministic, no telemetry: registry rules are fetched but no scan data
# is sent back.
SEMGREP_SEND_METRICS: "off"
steps:
- name: Harden the runner (Audit all outbound calls)
uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4
with:
egress-policy: audit
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false
- name: Run Semgrep (SARIF)
id: semgrep
run: |
set +e
echo "Using semgrep/semgrep:1.169.0@sha256:2b33f46ba66cf8cc2ad59ccfa7d22951fd00c632c38f1339e84ec8e6e641a942"
docker run --rm \
-v "${GITHUB_WORKSPACE}:/src" \
-w /src \
-e SEMGREP_SEND_METRICS=off \
--entrypoint semgrep \
semgrep/semgrep@sha256:2b33f46ba66cf8cc2ad59ccfa7d22951fd00c632c38f1339e84ec8e6e641a942 \
scan \
--config=p/default \
--severity=WARNING \
--severity=ERROR \
--exclude=.github/workflows \
--exclude='docs/research/**/standards' \
--error \
--sarif \
--output=semgrep-results.raw.sarif \
--metrics=off
echo "rc=$?" >> "$GITHUB_OUTPUT"
set -e
- name: Remove explicitly suppressed findings from Semgrep SARIF
id: semgrep_sarif
if: always() && hashFiles('semgrep-results.raw.sarif') != ''
run: |
set -euo pipefail
suppressed_count=$(jq '[.runs[]?.results[]? | select(((.suppressions // []) | length) > 0)] | length' semgrep-results.raw.sarif)
jq '(.runs[]? | .results) |= ((. // []) | map(select(((.suppressions // []) | length) == 0)))' \
semgrep-results.raw.sarif > semgrep-results.sarif
finding_count=$(jq '[.runs[]?.results[]?] | length' semgrep-results.sarif)
echo "suppressed_count=$suppressed_count" >> "$GITHUB_OUTPUT"
echo "finding_count=$finding_count" >> "$GITHUB_OUTPUT"
echo "SEMGREP_SUPPRESSED_COUNT=$suppressed_count SEMGREP_FINDING_COUNT=$finding_count"
- name: Upload Semgrep SARIF to code scanning
if: always() && hashFiles('semgrep-results.sarif') != ''
continue-on-error: true
uses: github/codeql-action/upload-sarif@99df26d4f13ea111d4ec1a7dddef6063f76b97e9 # v4.37.0
with:
sarif_file: semgrep-results.sarif
category: semgrep
- name: Report every Semgrep finding in the job log
if: always() && hashFiles('semgrep-results.sarif') != ''
env:
SEMGREP_RC: ${{ steps.semgrep.outputs.rc }}
SEMGREP_SUPPRESSED_COUNT: ${{ steps.semgrep_sarif.outputs.suppressed_count }}
run: |
set -euo pipefail
finding_count=$(jq '[.runs[]?.results[]?] | length' semgrep-results.sarif)
echo "SEMGREP_FINDING_COUNT=${finding_count} SEMGREP_SUPPRESSED_COUNT=${SEMGREP_SUPPRESSED_COUNT:-missing} SEMGREP_RC=${SEMGREP_RC:-missing}"
jq -r '
.runs[]? as $run
| ($run.tool.driver.rules // []
| map({key: .id, value: (.defaultConfiguration.level // "unknown")})
| from_entries) as $levels
| $run.results[]?
| (.locations[0].physicalLocation // {}) as $location
| "SEMGREP_FINDING rule=\(.ruleId // "unknown")"
+ " level=\(.level // $levels[.ruleId] // "unknown")"
+ " path=\($location.artifactLocation.uri // "unknown")"
+ " line=\($location.region.startLine // 0)"
+ " message=\((.message.text // "no message") | gsub("[\r\n]+"; " "))"
' semgrep-results.sarif
if [ "$finding_count" -eq 0 ] && [ "${SEMGREP_RC:-missing}" != "0" ]; then
echo "SEMGREP_ENGINE_FAILURE rc=${SEMGREP_RC:-missing}: Semgrep failed without a WARNING/ERROR SARIF result; inspect the scan command output above."
fi
- name: Enforce Semgrep gate (fail on Medium+ findings)
if: always() && (steps.semgrep_sarif.outputs.finding_count != '0' || steps.semgrep.outputs.rc != '0')
env:
SEMGREP_RC: ${{ steps.semgrep.outputs.rc }}
SEMGREP_FINDING_COUNT: ${{ steps.semgrep_sarif.outputs.finding_count }}
run: |
if [ "${SEMGREP_FINDING_COUNT:-missing}" != "0" ]; then
echo "::error::Semgrep found WARNING/ERROR (Medium+) findings. Every rule, path, line, and message is listed in the preceding report step and the 'semgrep' code scanning category."
else
echo "::error::Semgrep engine/configuration failed with rc=${SEMGREP_RC}. The concrete scan output and SARIF report are logged above."
fi
exit 1