Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/actionlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,3 +41,7 @@ paths:
ignore:
# Same gap again: it checks out auto-tag/ at job.workflow_sha.
- 'property "workflow_sha" is not defined in object type'
.github/workflows/semgrep-scan.yml:
ignore:
# Same gap again: it checks out semgrep/ at job.workflow_sha.
- 'property "workflow_sha" is not defined in object type'
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ updates:
# of ~30 repos is the same reason `cooldown` is omitted above — bumps
# here should not wait. See #127.
open-pull-requests-limit: 10
# Semgrep pin (semgrep/requirements.txt). No cooldown, as above. Callers
# get a bump only when the semgrep-scan-v1 tag moves. dev-env#166.
- package-ecosystem: "pip"
directory: "/semgrep"
schedule:
interval: "weekly"
commit-message:
prefix: "chore"
102 changes: 102 additions & 0 deletions .github/workflows/semgrep-scan.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
name: Semgrep Scan

# Semgrep CE, in place of CodeQL for private repos without GitHub Code
# Security (dev-env#166). Details: README.md.

# pull_request fails only on new findings (--baseline-commit). push and
# dispatch scan all. With SEMGREP_APP_TOKEN: `semgrep ci`.

# Caller stub: semgrep/caller-stub.yml. The caller grants contents: read
# and passes SEMGREP_APP_TOKEN by name, if it has one.

# No SARIF upload: code scanning on a private repo needs the GitHub Code
# Security SKU, which is what this workflow exists to avoid.

on:
workflow_call:
inputs:
rulesets:
description: >-
Semgrep configs, separated by spaces or newlines, each passed as
--config. Ignored when SEMGREP_APP_TOKEN is set.
type: string
default: p/default p/javascript p/typescript p/python p/ruby
timeout-minutes:
description: Job timeout.
type: number
default: 20
secrets:
SEMGREP_APP_TOKEN:
description: >-
Optional Semgrep platform token. When set, the scan runs as
`semgrep ci` and the platform's policies choose the rules.
required: false

permissions: {}

jobs:
semgrep:
runs-on: ubuntu-latest
permissions:
contents: read
timeout-minutes: ${{ inputs.timeout-minutes }}
env:
SEMGREP_SEND_METRICS: "off"
steps:
# Full history: --baseline-commit needs the PR base commit locally.
- name: Checkout caller repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
fetch-depth: 0
path: repo

# An empty ref makes checkout take the default branch, so fail instead.
# Same step as standards-check.yml.
- name: Resolve the workflow SHA
id: workflow_sha
env:
JOB_WORKFLOW_SHA: ${{ job.workflow_sha }}
run: |
set -euo pipefail
if [ -z "${JOB_WORKFLOW_SHA}" ]; then
echo "::error::job.workflow_sha is empty; refusing to check out an ambiguous ref"
exit 2
fi
echo "sha=${JOB_WORKFLOW_SHA}" >> "${GITHUB_OUTPUT}"
echo "workflow SHA: ${JOB_WORKFLOW_SHA}"

- name: Checkout semgrep scripts at the called-workflow SHA
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
repository: smartwatermelon/github-workflows
ref: ${{ steps.workflow_sha.outputs.sha }}
persist-credentials: false
fetch-depth: 1
path: semgrep-src
sparse-checkout: |
semgrep

# A venv: the runner's Python refuses a global pip install (PEP 668).
# Dependabot bumps the pin in semgrep/requirements.txt.
- name: Install Semgrep
env:
VENV: ${{ runner.temp }}/semgrep-venv
run: |
set -euo pipefail
python3 -m venv "${VENV}"
"${VENV}/bin/pip" install --disable-pip-version-check --quiet \
-r semgrep-src/semgrep/requirements.txt
"${VENV}/bin/semgrep" --version

- name: Run Semgrep
working-directory: repo
env:
SEMGREP_BIN: ${{ runner.temp }}/semgrep-venv/bin/semgrep
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
RULESETS: ${{ inputs.rulesets }}
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
run: |
set -euo pipefail
bash ../semgrep-src/semgrep/run-semgrep.sh
104 changes: 104 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,110 @@ Prefixed for the same reason as `standards-check-v*`.

---

## `semgrep-scan.yml`

Reusable static-analysis check with Semgrep Community Edition
([dev-env#166](https://github.com/smartwatermelon/dev-env/issues/166)). It
stands in for CodeQL on the private `nightowlstudiollc` repos, which do not
have GitHub Code Security. The pilot caller is nightowlstudiollc/reliquarist.

It is **not** a required check. On a pull request it fails only on findings
that are new in that PR, so existing findings in a repo do not turn every PR
red.

The triggering event and the secret pick the mode:

| Event | `SEMGREP_APP_TOKEN` | Runs |
| ----- | ------------------- | ---- |
| `pull_request` | absent or empty | `semgrep scan --error --config <each ruleset> --baseline-commit <PR base SHA>` |
| `push`, `workflow_dispatch` | absent or empty | `semgrep scan --error --config <each ruleset>` (whole tree) |
| any of the three | set | `semgrep ci` |

Any other event fails with exit 2, `pull_request_target` included.

`--baseline-commit` makes Semgrep report only findings that are not also
present at the PR base (`github.event.pull_request.base.sha`). The caller is
checked out with `fetch-depth: 0` so that commit is in the history. A run
whose base commit is missing fails with exit 2 instead of scanning the wrong
range. A full scan on `push` or `workflow_dispatch` fails on every finding,
old or new. Fix a repo's existing findings before you add the caller, so
its first `push` run on `main` is green.

**Token mode.** With `SEMGREP_APP_TOKEN` set, the workflow runs `semgrep ci`,
which reports to the Semgrep platform. The platform's policies then choose
the rules and the `rulesets` input is ignored. No repo uses a token today;
the secret is there so a repo can move to the platform later without a new
workflow. A PR from Dependabot or a fork gets no secrets, so it runs in scan
mode.

**No code-scanning upload.** The job has only `contents: read`: no
`security-events: write` and no SARIF upload. Code scanning on a private repo
needs the Code Security SKU this workflow exists to avoid. Findings appear in
the job log.

The mode logic is `semgrep/run-semgrep.sh`, checked out from this repo at
`job.workflow_sha`, as in `netlify-site-checks.yml`. Semgrep is installed
with pip into a venv from `semgrep/requirements.txt`, which pins one version;
Dependabot (`pip`, `/semgrep`) bumps it. `SEMGREP_SEND_METRICS=off` and
`--metrics=off` keep Semgrep from sending usage metrics. Registry rulesets
(`p/...`) need no token.

### Setup

`.github/workflows/semgrep-scan.yml` in the caller repo. Keep that file name
and the job name `semgrep-scan`: the daily digest will find callers by them.
The canonical copy is `semgrep/caller-stub.yml`:

```yaml
name: Semgrep Scan
on:
pull_request:
workflow_dispatch:
permissions:
contents: read
jobs:
semgrep-scan:
uses: smartwatermelon/github-workflows/.github/workflows/semgrep-scan.yml@semgrep-scan-v1
secrets:
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
```

- The status check is `semgrep-scan / semgrep` (caller job / inner job).
- Pass the secret by name, not with `secrets: inherit`. With no such secret
in the repo it is empty, and the run uses scan mode.
- To skip paths, add a `.semgrepignore` to the caller repo.

### Inputs

| Input | Type | Default | Description |
| ----- | ---- | ------- | ----------- |
| `rulesets` | string | `p/default p/javascript p/typescript p/python p/ruby` | Semgrep configs, separated by spaces or newlines, each passed as `--config`. Registry names or paths in the caller repo. Ignored in token mode. |
| `timeout-minutes` | number | `20` | Job timeout. |

| Secret | Required | Description |
| ------ | -------- | ----------- |
| `SEMGREP_APP_TOKEN` | no | Semgrep platform token. When set, the run is `semgrep ci` and the platform's policies replace `rulesets`. |

### Tests

`tests/test-semgrep-scan.sh` runs `semgrep/run-semgrep.sh` against a stub
`semgrep` in `tests/stub-semgrep/` and a throwaway git repo. It covers each
mode's arguments, that the token reaches Semgrep only through the
environment, the exit-2 refusals, and that Semgrep's exit code passes
through. It also checks that the `rulesets` default is the same in the
workflow, the caller stub, and this README, and that the stub keeps the
stable job name and `uses:` ref.

### Versioning

| Tag | Meaning |
| ----- | --------- |
| `semgrep-scan-v1` | Floating major for callers, moved manually and human-authorized, like `standards-check-v1`. |

Prefixed for the same reason as `standards-check-v*`.

---

## `claude-assistant` (deprecated)

`claude-assistant.yml` is deprecated. No fleet repo uses it except `kebab-tax`
Expand Down
18 changes: 18 additions & 0 deletions semgrep/caller-stub.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Semgrep Scan
on:
pull_request:
workflow_dispatch:
# A push run scans the whole tree and fails on any finding, old or new.
# Fix existing findings before adding this caller.
push:
branches: [main]
permissions:
contents: read
jobs:
semgrep-scan:
uses: smartwatermelon/github-workflows/.github/workflows/semgrep-scan.yml@semgrep-scan-v1
# with:
# rulesets: p/default p/javascript p/typescript p/python p/ruby # the default
secrets:
# Optional. Absent or empty: scan mode with the rulesets above.
SEMGREP_APP_TOKEN: ${{ secrets.SEMGREP_APP_TOKEN }}
1 change: 1 addition & 0 deletions semgrep/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
semgrep==1.182.0
62 changes: 62 additions & 0 deletions semgrep/run-semgrep.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#!/usr/bin/env bash
# Run Semgrep for semgrep-scan.yml, from the root of the caller checkout.

# Env: EVENT_NAME, BASE_SHA (PR only), RULESETS, SEMGREP_APP_TOKEN,
# SEMGREP_BIN. Exit: Semgrep's code (1 = findings), or 2 if misconfigured.

# With a token: `semgrep ci` (platform rules, not RULESETS). Else
# `semgrep scan --error`, plus --baseline-commit on pull_request.
set -euo pipefail

semgrep_bin="${SEMGREP_BIN:-semgrep}"
: "${EVENT_NAME:?EVENT_NAME is required}"

if ! command -v "${semgrep_bin}" >/dev/null 2>&1; then
echo "::error::semgrep not found at '${semgrep_bin}'" >&2
exit 2
fi

case "${EVENT_NAME}" in
pull_request | push | workflow_dispatch) ;;
*)
echo "::error::semgrep-scan runs on pull_request, push, or workflow_dispatch, not '${EVENT_NAME}'" >&2
exit 2
;;
esac

if [[ -n "${SEMGREP_APP_TOKEN:-}" ]]; then
echo "::notice::SEMGREP_APP_TOKEN is set: running semgrep ci; platform policies replace the rulesets input." >&2
exec "${semgrep_bin}" ci
fi

read -r -d '' -a rulesets <<<"${RULESETS:-}" || true
if [[ "${#rulesets[@]}" -eq 0 ]]; then
echo "::error::RULESETS is empty; give at least one Semgrep config" >&2
exit 2
fi
args=(scan --error --metrics=off)
for r in "${rulesets[@]}"; do
# A registry name (p/default, r/...) or a path in the caller repo.
if [[ ! "${r}" =~ ^[A-Za-z0-9_.][A-Za-z0-9_./:@-]*$ ]]; then
echo "::error::ruleset '${r}' is not a valid Semgrep config name" >&2
exit 2
fi
args+=(--config "${r}")
done

if [[ "${EVENT_NAME}" == "pull_request" ]]; then
if [[ ! "${BASE_SHA:-}" =~ ^[0-9a-f]{40}$ ]]; then
echo "::error::pull_request run without a valid BASE_SHA ('${BASE_SHA:-}')" >&2
exit 2
fi
if ! git cat-file -e "${BASE_SHA}^{commit}" 2>/dev/null; then
echo "::error::base commit ${BASE_SHA} is not in this checkout; check out with fetch-depth: 0" >&2
exit 2
fi
echo "Diff-aware scan: only findings new since ${BASE_SHA} fail." >&2
args+=(--baseline-commit "${BASE_SHA}")
else
echo "Full scan (${EVENT_NAME})." >&2
fi

exec "${semgrep_bin}" "${args[@]}"
8 changes: 8 additions & 0 deletions tests/stub-semgrep/semgrep
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# Stub semgrep for test-semgrep-scan.sh: writes argv to argv.log and
# SEMGREP_APP_TOKEN to token.log, then exits STUB_RC.
set -euo pipefail
: "${STUB_DIR:?}"
printf '%s\n' "$@" >"${STUB_DIR}/argv.log"
printf '%s\n' "${SEMGREP_APP_TOKEN-}" >"${STUB_DIR}/token.log"
exit "${STUB_RC:-0}"
Loading
Loading