diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..1f3611c --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,3 @@ +/.github/workflows/required-checks.yml @PrunaAI/safety +/.github/scripts/check_run_secrets.py @PrunaAI/safety +/.github/CODEOWNERS @PrunaAI/safety diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..988b02a --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,20 @@ +version: 2 +updates: + - package-ecosystem: github-actions + cooldown: + default-days: 7 + directories: + - "/" + schedule: + interval: weekly + groups: + minor-and-patch: + patterns: ["*"] + update-types: [minor, patch] + - package-ecosystem: docker + directories: + - "/.github/workflows" + schedule: + interval: weekly + cooldown: + default-days: 7 diff --git a/.github/scripts/check_run_secrets.py b/.github/scripts/check_run_secrets.py new file mode 100644 index 0000000..8cd59ca --- /dev/null +++ b/.github/scripts/check_run_secrets.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""Fail if a workflow `run:` step interpolates `${{ secrets.* }}` directly into the shell. + +Interpolating a secret into a run script leaks it via shell history, process listings +and command echoing, and opens script injection. Pass it through `env:` and reference +the environment variable inside the script instead (see PrunaAI/prunatree#641). +""" +import pathlib +import re +import sys + +import yaml + +SECRET_PATTERN = re.compile(r"\$\{\{\s*secrets\.") + + +def iter_run_steps(workflow: dict): + for job in (workflow.get("jobs") or {}).values(): + if not isinstance(job, dict): + continue + for step in job.get("steps") or []: + run = step.get("run") if isinstance(step, dict) else None + if isinstance(run, str): + yield step.get("name", ""), run + + +def main() -> int: + violations = [] + for path in sorted(pathlib.Path(".github/workflows").glob("*.y*ml")): + workflow = yaml.safe_load(path.read_text()) + if not isinstance(workflow, dict): + continue + for name, run in iter_run_steps(workflow): + if SECRET_PATTERN.search(run): + violations.append( + f"{path}: step {name!r} interpolates a secret directly into `run:` " + "— pass it through `env:` and reference the env var instead" + ) + + if violations: + print("\n".join(violations)) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/required-checks.yml b/.github/workflows/required-checks.yml new file mode 100644 index 0000000..8aaead0 --- /dev/null +++ b/.github/workflows/required-checks.yml @@ -0,0 +1,26 @@ +name: required checks + +on: + pull_request: + +permissions: + contents: read + +jobs: + required-checks: + runs-on: ubuntu-latest + container: + image: semgrep/semgrep:1.178.0@sha256:32e459968daabe7ab86968184a29109b9564aa00392401156f9788452b42786b + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0 + with: + fetch-depth: 0 + - name: Semgrep (only findings this pull request adds) + run: | + git config --global --add safe.directory "$GITHUB_WORKSPACE" + semgrep scan --config p/trailofbits --config p/default --severity ERROR --error --metrics off \ + --baseline-commit "${{ github.event.pull_request.base.sha }}" + - name: No secrets interpolated directly into run steps + run: | + apk add --no-cache py3-yaml + python3 .github/scripts/check_run_secrets.py diff --git a/dependabot.yaml b/dependabot.yaml deleted file mode 100644 index ec85b2e..0000000 --- a/dependabot.yaml +++ /dev/null @@ -1,12 +0,0 @@ -version: 2 - -updates: - - package-ecosystem: "github-actions" - directory: "/" - schedule: - interval: "weekly" - - - package-ecosystem: "pip" - directory: "/" - schedule: - interval: "weekly"