From 305e39be67cc3a3fa37f1f9729ca1f27c735a8be Mon Sep 17 00:00:00 2001 From: ssvoss Date: Thu, 29 Jan 2026 16:58:51 -0500 Subject: [PATCH 1/4] update semgrep GHA to call repo qualified actions so it can be called from other repos --- .github/workflows/run_semgrep_scan.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/run_semgrep_scan.yml b/.github/workflows/run_semgrep_scan.yml index 0d54de8..3e5943a 100644 --- a/.github/workflows/run_semgrep_scan.yml +++ b/.github/workflows/run_semgrep_scan.yml @@ -101,7 +101,7 @@ jobs: - name: Check for open PR (by commit) id: pr_check - uses: ./.github/actions/pr-open-check + uses: OpenSesame/core-github-actions/.github/actions/pr-open-check@actions/pr-open-check/2.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} commit-identifier: ${{ inputs.commit_identifier }} @@ -265,7 +265,7 @@ jobs: - name: Upsert PR comment if: ${{ github.event_name == 'pull_request' || steps.pr_check.outputs.pr_exists == 'true' }} - uses: ./.github/actions/upsert-pr-comment + uses: OpenSesame/core-github-actions/.github/actions/upsert-pr-comment@actions/upsert-pr-comment/1.0.0 with: pr-number: ${{ steps.pr_check.outputs.pr_number }} github-token: ${{ secrets.GITHUB_TOKEN }} From 410a3075b65c06384ef84a70a8d4cce45b141bf9 Mon Sep 17 00:00:00 2001 From: ssvoss Date: Thu, 29 Jan 2026 17:06:34 -0500 Subject: [PATCH 2/4] update int --- .github/actions/TEMPLATE/README_TEMPLATE.md | 2 +- .github/actions/pr-open-check/README.md | 2 +- .github/actions/upsert-pr-comment/README.md | 2 +- .github/workflows/CHANGELOGS/run_semgrep_scan.md | 6 ++++++ 4 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/actions/TEMPLATE/README_TEMPLATE.md b/.github/actions/TEMPLATE/README_TEMPLATE.md index 06f62a9..6232250 100644 --- a/.github/actions/TEMPLATE/README_TEMPLATE.md +++ b/.github/actions/TEMPLATE/README_TEMPLATE.md @@ -52,7 +52,7 @@ Basic usage example: ```yaml - name: Name for step id: - uses: ./.github/actions/ + uses: OpenSesame/core-github-actions/.github/actions/@actions//vX.Y.Z with: : ``` diff --git a/.github/actions/pr-open-check/README.md b/.github/actions/pr-open-check/README.md index 7e783c6..c56a070 100644 --- a/.github/actions/pr-open-check/README.md +++ b/.github/actions/pr-open-check/README.md @@ -53,7 +53,7 @@ permissions: ```yaml - name: Check for open PR id: pr_check - uses: ./.github/actions/pr-check-open + uses: OpenSesame/core-github-actions/.github/actions/pr-open-check@actions/pr-open-check/2.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} commit-identifier: ${{ github.sha }} diff --git a/.github/actions/upsert-pr-comment/README.md b/.github/actions/upsert-pr-comment/README.md index 125507d..35f438c 100644 --- a/.github/actions/upsert-pr-comment/README.md +++ b/.github/actions/upsert-pr-comment/README.md @@ -46,7 +46,7 @@ Basic usage example: ```yaml - name: Upsert PR summary comment - uses: ./.github/actions/upsert-pr-comment + uses: OpenSesame/core-github-actions/.github/actions/upsert-pr-comment@actions/upsert-pr-comment/1.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} pr-number: ${{ github.event.pull_request.number }} diff --git a/.github/workflows/CHANGELOGS/run_semgrep_scan.md b/.github/workflows/CHANGELOGS/run_semgrep_scan.md index 7e432a7..2f38dc9 100644 --- a/.github/workflows/CHANGELOGS/run_semgrep_scan.md +++ b/.github/workflows/CHANGELOGS/run_semgrep_scan.md @@ -2,6 +2,12 @@ All notable changes to the **run_semgrep_scan** callable workflow are documented in this file. +## 1.0.1 + +### Fixed + +- Repo-qualified internal action references to ensure correct resolution when this workflow is called from other repositories. This change allows the workflow to reliably locate and use the intended actions, regardless of the calling repository context. + ## 1.0.0 ### Added From 35c76661eafda4a77761a4bce61aad5dea285e22 Mon Sep 17 00:00:00 2001 From: ssvoss Date: Thu, 29 Jan 2026 17:38:43 -0500 Subject: [PATCH 3/4] semgrep checkout the action repo in a subdir to access actions and scripts --- .../workflows/CHANGELOGS/run_semgrep_scan.md | 4 +-- .github/workflows/run_semgrep_scan.yml | 33 ++++++++++++++++--- 2 files changed, 31 insertions(+), 6 deletions(-) diff --git a/.github/workflows/CHANGELOGS/run_semgrep_scan.md b/.github/workflows/CHANGELOGS/run_semgrep_scan.md index 2f38dc9..6808ba9 100644 --- a/.github/workflows/CHANGELOGS/run_semgrep_scan.md +++ b/.github/workflows/CHANGELOGS/run_semgrep_scan.md @@ -4,9 +4,9 @@ All notable changes to the **run_semgrep_scan** callable workflow are documented ## 1.0.1 -### Fixed +### Changed -- Repo-qualified internal action references to ensure correct resolution when this workflow is called from other repositories. This change allows the workflow to reliably locate and use the intended actions, regardless of the calling repository context. +- Updated workflow to support cross-repository usage by checking out the core-github-actions repository into a subdirectory and referencing all internal actions and scripts from that subdirectory. This ensures that required actions and scripts are always available, regardless of which repository invokes the workflow. ## 1.0.0 diff --git a/.github/workflows/run_semgrep_scan.yml b/.github/workflows/run_semgrep_scan.yml index 3e5943a..df31aca 100644 --- a/.github/workflows/run_semgrep_scan.yml +++ b/.github/workflows/run_semgrep_scan.yml @@ -92,16 +92,41 @@ jobs: normalized_baseline: ${{ steps.semgrep.outputs.normalizedBaseline }} steps: - - name: Checkout code + - name: Checkout Calling Repo uses: actions/checkout@v4 with: ref: ${{ inputs.commit_identifier }} # Full history only when diff/baseline is requested fetch-depth: ${{ inputs.semgrep_scan_mode == 'full' && '1' || '0' }} + - name: Extract workflow definition repo and ref + id: extract_workflow_details + run: | + WF_REF="${{ github.workflow_ref }}" + echo "github.workflow_ref: $WF_REF" + REPO="${WF_REF%%/.github/workflows/*}" + REF="${WF_REF##*@}" + echo "Extracted repo: $REPO" + echo "Extracted ref: $REF" + echo "repo=$REPO" >> $GITHUB_OUTPUT + echo "ref=$REF" >> $GITHUB_OUTPUT + + - name: Checkout workflow repo for access to internal scripts/actions + uses: actions/checkout@v4 + with: + repository: ${{ steps.extract_workflow_details.outputs.repo }} + ref: ${{ steps.extract_workflow_details.outputs.ref }} + path: workflow-repo + + - run: | + ls + echo "-----" + cd workflow-repo + ls + - name: Check for open PR (by commit) id: pr_check - uses: OpenSesame/core-github-actions/.github/actions/pr-open-check@actions/pr-open-check/2.0.0 + uses: ./workflow-repo/.github/actions/pr-open-check with: github-token: ${{ secrets.GITHUB_TOKEN }} commit-identifier: ${{ inputs.commit_identifier }} @@ -135,7 +160,7 @@ jobs: SEMGREP_TARGETS: ${{ inputs.semgrep_targets }} FAIL_LEVEL: ${{ inputs.fail_severity }} EXTRA_ARGS: ${{ inputs.extra_args }} - run: node scripts/gha-lib/run-semgrep.js + run: node ./workflow-repo/scripts/gha-lib/run-semgrep.js - name: Upload Artifact if: ${{ steps.semgrep.outputs.totalFindings > 0 }} @@ -265,7 +290,7 @@ jobs: - name: Upsert PR comment if: ${{ github.event_name == 'pull_request' || steps.pr_check.outputs.pr_exists == 'true' }} - uses: OpenSesame/core-github-actions/.github/actions/upsert-pr-comment@actions/upsert-pr-comment/1.0.0 + uses: ./workflow-repo/.github/actions/upsert-pr-comment with: pr-number: ${{ steps.pr_check.outputs.pr_number }} github-token: ${{ secrets.GITHUB_TOKEN }} From e6d8014d7ff49b1abbeae4f951ba3559b344205f Mon Sep 17 00:00:00 2001 From: ssvoss Date: Fri, 30 Jan 2026 13:15:42 -0500 Subject: [PATCH 4/4] move run-semgrep script into composite action to facilitate calling workflow cross-repo --- .github/actions/run-semgrep/.npmrc | 4 + .github/actions/run-semgrep/CHANGELOG.md | 14 + .github/actions/run-semgrep/README.md | 113 ++++++ .github/actions/run-semgrep/action.yml | 13 + .../actions/run-semgrep}/env-helpers.js | 0 .../run-semgrep}/env-helpers.unit.test.js | 0 .github/actions/run-semgrep/package-lock.json | 57 +++ .github/actions/run-semgrep/package.json | 10 + .../actions/run-semgrep}/run-semgrep.js | 36 +- .../run-semgrep/run-semgrep.unit.test.js | 33 ++ .github/workflows/run_semgrep_scan.yml | 31 +- .vscode/settings.json | 2 +- package.json | 4 +- scripts/gha-lib/run-semgrep.unit.test.js | 347 ------------------ .../index.integration.test.js | 2 +- .../index.integration.test.js | 2 +- .../{utils => internal-utils}/test-helpers.js | 0 17 files changed, 253 insertions(+), 415 deletions(-) create mode 100644 .github/actions/run-semgrep/.npmrc create mode 100644 .github/actions/run-semgrep/CHANGELOG.md create mode 100644 .github/actions/run-semgrep/README.md create mode 100644 .github/actions/run-semgrep/action.yml rename {scripts/utils => .github/actions/run-semgrep}/env-helpers.js (100%) rename {scripts/utils => .github/actions/run-semgrep}/env-helpers.unit.test.js (100%) create mode 100644 .github/actions/run-semgrep/package-lock.json create mode 100644 .github/actions/run-semgrep/package.json rename {scripts/gha-lib => .github/actions/run-semgrep}/run-semgrep.js (84%) create mode 100644 .github/actions/run-semgrep/run-semgrep.unit.test.js delete mode 100644 scripts/gha-lib/run-semgrep.unit.test.js rename scripts/{utils => internal-utils}/test-helpers.js (100%) diff --git a/.github/actions/run-semgrep/.npmrc b/.github/actions/run-semgrep/.npmrc new file mode 100644 index 0000000..06f2f9a --- /dev/null +++ b/.github/actions/run-semgrep/.npmrc @@ -0,0 +1,4 @@ +ignore-scripts=true +save-exact=true +audit=true +fund=false diff --git a/.github/actions/run-semgrep/CHANGELOG.md b/.github/actions/run-semgrep/CHANGELOG.md new file mode 100644 index 0000000..0f8bc01 --- /dev/null +++ b/.github/actions/run-semgrep/CHANGELOG.md @@ -0,0 +1,14 @@ +# Changelog for run-semgrep Composite Action + +All notable changes to the run-semgrep composite GitHub Action will be documented in this file. + +## 1.0.0 - Initial Release + +### Added + +- Initial release of the reusable composite action for running Semgrep scans +- Inputs are passed via environment variables +- Support running on both push and pull_request events +- Standardizes baseline resolution for diff scans +- Outputs include scan summary, config summary, scan status, and finding counts +- Designed to integrate with reviewdog for annotations diff --git a/.github/actions/run-semgrep/README.md b/.github/actions/run-semgrep/README.md new file mode 100644 index 0000000..73f6b40 --- /dev/null +++ b/.github/actions/run-semgrep/README.md @@ -0,0 +1,113 @@ +# Run Semgrep Action + +## 🧭 Summary + +Runs a Semgrep scan normalizing the baseline for diff scans depending on push vs PR context. Outputs scan results and summaries for downstream steps. + +## Scope/Limitations + +- Supports both push and pull request events. +- Requires Semgrep to be installed and available in the runner environment. +- Expects environment variables for configuration (see below). + +## 🔒 Permissions + +The following GHA permissions are required to use this step: + +```yaml +permissions: + contents: read +``` + +## Dependencies + +- `semgrep` — must be installed in the runner environment. +- `node-fetch` — required Node.js dependency (see package.json). +- `reviewdog` — for annotation output (optional, for downstream steps). + +## ⚙️ Inputs + +This action is environment-driven. The following environment variables are required: + +| Name | Required | Description | +| ------------------- | -------- | ------------------------------------------------------------------------------------------- | +| `HAS_PR` | ✅ | Whether the current context has an associated PR (true/false) | +| `PR_NUMBER` | ❌ | PR number if applicable | +| `PR_URL` | ❌ | PR URL if applicable | +| `INPUT_BASELINE` | ✅ | Baseline ref to use for diffing (e.g., origin/main) | +| `GITHUB_EVENT_NAME` | ✅ | GitHub provided environment variable for event name (e.g., push, pull_request) | +| `GITHUB_REF_NAME` | ✅ | GitHub provided environment variable for the branch or tag name that triggered the workflow | +| `GITHUB_BASE_REF` | ❌ | GitHub provided environment variable for the base ref of a PR (if applicable) | +| `GITHUB_REPOSITORY` | ✅ | GitHub provided environment variable for the repository (e.g., owner/repo) | +| `GITHUB_TOKEN` | ✅ | GitHub token for API access | +| `SCAN_MODE` | ✅ | 'diff' or 'full' scan mode | +| `SEMGREP_CONFIG` | ✅ | Semgrep ruleset(s) to use | +| `SEMGREP_TARGETS` | ✅ | Targets to scan (default: current directory) | +| `FAIL_LEVEL` | ✅ | Severity level to fail on (e.g., ERROR, WARNING) | +| `EXTRA_ARGS` | ❌ | Additional arguments to pass to Semgrep | + +## 📤 Outputs + +Along with writing files for reviewdog annotations and inputs, this action provides the following outputs: + +| Name | Description | +| -------------------- | --------------------------------------------------- | +| `normalizedBaseline` | The resolved baseline ref | +| `scanSummary` | Summary of findings in markdown format | +| `configSummary` | Summary of scan config in markdown format | +| `scanStatus` | 'success' or 'failure' based on findings/fail level | +| `totalFindings` | Total number of findings | +| `numErrors` | Number of ERROR severity findings | +| `numWarnings` | Number of WARNING severity findings | +| `numInfo` | Number of INFO severity findings | + +## 🚀 Usage + +Basic usage example: + +```yaml +- name: Run Semgrep + id: semgrep + uses: OpenSesame/core-github-actions/.github/actions/run-semgrep@actions/run-semgrep/1.0.0 + env: + HAS_PR: ${{ env.HAS_PR }} + INPUT_BASELINE: ${{ env.INPUT_BASELINE }} + GITHUB_EVENT_NAME: ${{ github.event_name }} + GITHUB_REF_NAME: ${{ github.ref_name }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY: ${{ github.repository }} + SEMGREP_CONFIG: 'p/default' + SEMGREP_TARGETS: '.' + SCAN_MODE: 'full' + FAIL_LEVEL: 'error' + EXTRA_ARGS: '' +``` + +Example outputs: + +```yaml +steps.semgrep.outputs.scanStatus +steps.semgrep.outputs.totalFindings +``` + +Example usage of outputs in later steps: + +```yaml +if: steps.semgrep.outputs.scanStatus == 'failure' + run: echo "Semgrep scan failed at or above threshold." +``` + +## 🧠 Notes + +- This action writes a file for reviewdog annotations (`reviewdog_input.txt`). +- Unit tests for the script are included in `run-semgrep.unit.test.js` (not used by the action, but kept for maintainability). + +## Versioning + +This action uses namespaced tags for versioning and is tracked in the CHANGELOG. + +```text +actions/run-semgrep/vX.Y.Z +``` + +See the repository's versioning documentation for details on how tags are validated and created. diff --git a/.github/actions/run-semgrep/action.yml b/.github/actions/run-semgrep/action.yml new file mode 100644 index 0000000..4d0ff00 --- /dev/null +++ b/.github/actions/run-semgrep/action.yml @@ -0,0 +1,13 @@ +name: 'Run Semgrep' +description: 'Run a Semgrep scan and output results for reviewdog and future steps' +runs: + using: composite + steps: + - name: Install action dependencies + shell: bash + working-directory: ${{ github.action_path }} + run: npm ci + + - name: Run Semgrep Scan + shell: bash + run: node ${{ github.action_path }}/run-semgrep.js diff --git a/scripts/utils/env-helpers.js b/.github/actions/run-semgrep/env-helpers.js similarity index 100% rename from scripts/utils/env-helpers.js rename to .github/actions/run-semgrep/env-helpers.js diff --git a/scripts/utils/env-helpers.unit.test.js b/.github/actions/run-semgrep/env-helpers.unit.test.js similarity index 100% rename from scripts/utils/env-helpers.unit.test.js rename to .github/actions/run-semgrep/env-helpers.unit.test.js diff --git a/.github/actions/run-semgrep/package-lock.json b/.github/actions/run-semgrep/package-lock.json new file mode 100644 index 0000000..3ec4136 --- /dev/null +++ b/.github/actions/run-semgrep/package-lock.json @@ -0,0 +1,57 @@ +{ + "name": "@opensesame/run-semgrep-action", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@opensesame/run-semgrep-action", + "version": "1.0.0", + "dependencies": { + "node-fetch": "^2.6.7" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + } + } +} diff --git a/.github/actions/run-semgrep/package.json b/.github/actions/run-semgrep/package.json new file mode 100644 index 0000000..2de2352 --- /dev/null +++ b/.github/actions/run-semgrep/package.json @@ -0,0 +1,10 @@ +{ + "name": "@opensesame/run-semgrep-action", + "version": "1.0.0", + "main": "run-semgrep.js", + "private": true, + "description": "Composite action to run Semgrep scan for GitHub Actions.", + "dependencies": { + "node-fetch": "^2.6.7" + } +} diff --git a/scripts/gha-lib/run-semgrep.js b/.github/actions/run-semgrep/run-semgrep.js similarity index 84% rename from scripts/gha-lib/run-semgrep.js rename to .github/actions/run-semgrep/run-semgrep.js index e1131d6..9aa9d7c 100644 --- a/scripts/gha-lib/run-semgrep.js +++ b/.github/actions/run-semgrep/run-semgrep.js @@ -1,41 +1,7 @@ -/* - * Run Semgrep scan - * Normalizes baseline for diff scans depending on push vs PR context - * - * Expects the following environment variables: - * HAS_PR - whether the current context has an associated PR (true/false) - * PR_NUMBER - PR number if applicable - * PR_URL - PR URL if applicable - * INPUT_BASELINE - baseline ref to use for diffing (e.g., origin/main) - * GITHUB_EVENT_NAME - GitHub provided environment variable for event name (e.g., push, pull_request) - * GITHUB_REF - Github provided environment variable for the git ref that triggered the workflow - * GITHUB_REF_NAME - GitHub provided environment variable for the branch or tag name that triggered the workflow - * GITHUB_BASE_REF - GitHub provided environment variable for the base ref of a PR (if applicable) - * GITHUB_REPOSITORY - GitHub provided environment variable for the repository (e.g., owner/repo) - * GITHUB_TOKEN - GitHub token for API access - * SCAN_MODE - 'diff' or 'full' scan mode - * SEMGREP_CONFIG - Semgrep ruleset(s) to use - * SEMGREP_TARGETS - Targets to scan (default: current directory) - * FAIL_LEVEL - Severity level to fail on (e.g., ERROR, WARNING) - * EXTRA_ARGS - Additional arguments to pass to Semgrep - * - * Outputs: - * - Writes file for reviewdog annotations, reviewdog_input.txt - * - Sets GitHub Action outputs - * - normalizedBaseline - the resolved baseline ref - * - totalFindings - total number of findings - * - numErrors - number of ERROR severity findings - * - numWarnings - number of WARNING severity findings - * - numInfo - number of INFO severity findings - * - scanSummary - summary of findings in md format - * - configSummary - summary of scan config in md format - * - scanStatus - 'success' or 'failure' based on findings and fail level - */ - const { spawnSync } = require('child_process'); const fs = require('fs'); const fetch = require('node-fetch'); -const { validateEnvVar } = require('../utils/env-helpers'); +const { validateEnvVar } = require('./env-helpers'); const SEMGREP_RESULTS_FILE_NAME = 'semgrep_results.json'; const REVIEWDOG_INPUT_FILE_NAME = 'reviewdog_input.txt'; diff --git a/.github/actions/run-semgrep/run-semgrep.unit.test.js b/.github/actions/run-semgrep/run-semgrep.unit.test.js new file mode 100644 index 0000000..73c0020 --- /dev/null +++ b/.github/actions/run-semgrep/run-semgrep.unit.test.js @@ -0,0 +1,33 @@ +const { validateEnvVar } = require('./env-helpers'); + +describe('validateEnvVar', () => { + const ORIGINAL_EXIT = process.exit; + const ORIGINAL_CONSOLE_ERROR = console.error; + + beforeEach(() => { + process.exit = jest.fn(); + console.error = jest.fn(); + }); + + afterEach(() => { + process.exit = ORIGINAL_EXIT; + console.error = ORIGINAL_CONSOLE_ERROR; + }); + + it('does not exit when env var is set', () => { + process.env.TEST_VAR = 'value'; + validateEnvVar('TEST_VAR'); + expect(process.exit).not.toHaveBeenCalled(); + expect(console.error).not.toHaveBeenCalled(); + delete process.env.TEST_VAR; + }); + + it('exits with error when env var is not set', () => { + delete process.env.TEST_VAR; + validateEnvVar('TEST_VAR'); + expect(console.error).toHaveBeenCalledWith( + '::error::Environment variable TEST_VAR is required' + ); + expect(process.exit).toHaveBeenCalledWith(1); + }); +}); diff --git a/.github/workflows/run_semgrep_scan.yml b/.github/workflows/run_semgrep_scan.yml index df31aca..60b6b79 100644 --- a/.github/workflows/run_semgrep_scan.yml +++ b/.github/workflows/run_semgrep_scan.yml @@ -99,34 +99,9 @@ jobs: # Full history only when diff/baseline is requested fetch-depth: ${{ inputs.semgrep_scan_mode == 'full' && '1' || '0' }} - - name: Extract workflow definition repo and ref - id: extract_workflow_details - run: | - WF_REF="${{ github.workflow_ref }}" - echo "github.workflow_ref: $WF_REF" - REPO="${WF_REF%%/.github/workflows/*}" - REF="${WF_REF##*@}" - echo "Extracted repo: $REPO" - echo "Extracted ref: $REF" - echo "repo=$REPO" >> $GITHUB_OUTPUT - echo "ref=$REF" >> $GITHUB_OUTPUT - - - name: Checkout workflow repo for access to internal scripts/actions - uses: actions/checkout@v4 - with: - repository: ${{ steps.extract_workflow_details.outputs.repo }} - ref: ${{ steps.extract_workflow_details.outputs.ref }} - path: workflow-repo - - - run: | - ls - echo "-----" - cd workflow-repo - ls - - name: Check for open PR (by commit) id: pr_check - uses: ./workflow-repo/.github/actions/pr-open-check + uses: OpenSesame/core-github-actions/.github/actions/pr-open-check@actions/pr-open-check/2.0.0 with: github-token: ${{ secrets.GITHUB_TOKEN }} commit-identifier: ${{ inputs.commit_identifier }} @@ -151,6 +126,7 @@ jobs: - name: Run Semgrep id: semgrep + uses: OpenSesame/core-github-actions/.github/actions/run-semgrep@actions/run-semgrep/1.0.0 env: INPUT_BASELINE: ${{ inputs.baseline_ref }} HAS_PR: ${{ steps.pr_check.outputs.pr_exists }} @@ -160,7 +136,6 @@ jobs: SEMGREP_TARGETS: ${{ inputs.semgrep_targets }} FAIL_LEVEL: ${{ inputs.fail_severity }} EXTRA_ARGS: ${{ inputs.extra_args }} - run: node ./workflow-repo/scripts/gha-lib/run-semgrep.js - name: Upload Artifact if: ${{ steps.semgrep.outputs.totalFindings > 0 }} @@ -290,7 +265,7 @@ jobs: - name: Upsert PR comment if: ${{ github.event_name == 'pull_request' || steps.pr_check.outputs.pr_exists == 'true' }} - uses: ./workflow-repo/.github/actions/upsert-pr-comment + uses: OpenSesame/core-github-actions/.github/actions/upsert-pr-comment@actions/upsert-pr-comment/1.0.0 with: pr-number: ${{ steps.pr_check.outputs.pr_number }} github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.vscode/settings.json b/.vscode/settings.json index ef7602b..dac8c99 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -43,5 +43,5 @@ // Optional: Spell checker "cSpell.enabled": true, - "cSpell.words": ["opensesame", "reviewdog", "semgrep", "upserting"] + "cSpell.words": ["nosemgrep", "opensesame", "reviewdog", "semgrep", "upserting"] } diff --git a/package.json b/package.json index 2868b5b..7ad7654 100644 --- a/package.json +++ b/package.json @@ -27,8 +27,8 @@ "audit": "npm audit --audit-level=high --omit=dev", "lint:check": "eslint *.js --ext .js,.json", "lint:fix": "eslint *.js --ext .js,.json --fix", - "format:check": "prettier --check './*.js' './*.mjs' './*.json' './*.md' 'scripts/**/*.js' '.github/actions/**/*.*'", - "format:fix": "prettier --write './*.js' './*.mjs' './*.json' './*.md' 'scripts/**/*.js' '.github/actions/**/*.*'", + "format:check": "prettier --check './*.js' './*.mjs' './*.json' './*.md' 'scripts/**/*.js' '.github/actions/**/*.yml' '.github/actions/**/*.md' '.github/actions/**/*.js'", + "format:fix": "prettier --write './*.js' './*.mjs' './*.json' './*.md' 'scripts/**/*.js' '.github/actions/**/*.yml' '.github/actions/**/*.md' '.github/actions/**/*.js'", "scan": "semgrep --config=p/ci --config=p/security-audit --config=p/javascript ./*.js ./*.mjs ./*.json scripts/ .github/actions/", "check": "npm run audit && npm run test && npm run lint:check && npm run format:check && npm run scan" }, diff --git a/scripts/gha-lib/run-semgrep.unit.test.js b/scripts/gha-lib/run-semgrep.unit.test.js deleted file mode 100644 index 6871f03..0000000 --- a/scripts/gha-lib/run-semgrep.unit.test.js +++ /dev/null @@ -1,347 +0,0 @@ -const fetch = require('node-fetch'); -const fs = require('fs'); -const { - getPrBaseBranch, - normalizeBaseline, - constructSemgrepCommand, - stageResultsForReviewdog, - getSemgrepMetrics, - writeFindingsMarkdown, - writeConfigMarkdown, - evaluateScanStatus, - REVIEWDOG_INPUT_FILE_NAME, -} = require('./run-semgrep'); - -const exampleSemgrepOutput = { - results: [ - { - path: 'src/error.js', - end: { line: 111 }, - extra: { - severity: 'ERROR', - message: 'This is an error message', - }, - }, - { - path: 'src/warning1.js', - end: { line: 222 }, - extra: { - severity: 'WARNING', - message: 'This is a warning message #1', - }, - }, - { - path: 'src/info.js', - end: { line: 333 }, - extra: { - severity: 'INFO', - message: 'This is an info message', - }, - }, - { - path: 'src/warning2.js', - end: { line: 444 }, - extra: { - severity: 'WARNING', - message: 'This is a warning message #2', - }, - }, - ], -}; -const emptySemgrepOutput = '{"results":[]}'; - -jest.mock('node-fetch'); - -describe('getPrBaseBranch', () => { - const OWNER = 'test-owner'; - const REPO = 'test-repo'; - const BRANCH = 'feature-branch'; - const TOKEN = 'ghp_testtoken'; - - afterEach(() => { - fetch.mockClear(); - }); - - it('returns base branch when PR exists', async () => { - const mockResponse = [ - { - base: { ref: 'main' }, - }, - ]; - fetch.mockResolvedValue({ - ok: true, - json: async () => mockResponse, - }); - - const baseBranch = await getPrBaseBranch(OWNER, REPO, BRANCH, TOKEN); - expect(baseBranch).toBe('main'); - }); - - it('returns null when no PR exists', async () => { - fetch.mockResolvedValue({ - ok: true, - json: async () => [], - }); - - const baseBranch = await getPrBaseBranch(OWNER, REPO, BRANCH, TOKEN); - expect(baseBranch).toBeNull(); - }); - - it('returns null on fetch error', async () => { - fetch.mockResolvedValue({ - ok: false, - }); - - const baseBranch = await getPrBaseBranch(OWNER, REPO, BRANCH, TOKEN); - expect(baseBranch).toBeNull(); - }); -}); - -describe('normalizeBaseline', () => { - const FULL_REPO_NAME = 'repo-owner/test-repo'; - const GITHUB_TOKEN = 'ghp_testtoken'; - const inputBaseline = 'origin/main'; - - afterEach(() => { - fetch.mockClear(); - }); - - it('returns input baseline for non-PR event', async () => { - const hasPr = 'false'; - const githubDetails = { - eventName: 'push', - baseRef: '', - githubRefName: 'feature-branch', - githubToken: GITHUB_TOKEN, - repo: FULL_REPO_NAME, - }; - const baseline = await normalizeBaseline(hasPr, inputBaseline, githubDetails); - - expect(baseline).toBe('origin/main'); - }); - - it('returns origin/baseRef for PR event with baseRef', async () => { - const hasPr = 'true'; - const githubDetails = { - eventName: 'pull_request', - baseRef: 'develop', - githubRefName: 'feature-branch', - githubToken: GITHUB_TOKEN, - repo: FULL_REPO_NAME, - }; - - const baseline = await normalizeBaseline(hasPr, inputBaseline, githubDetails); - - expect(baseline).toBe(`origin/${githubDetails.baseRef}`); - }); - - it('fetches base branch when baseRef is not provided', async () => { - const mockResponse = [ - { - base: { ref: 'staging' }, - }, - ]; - fetch.mockResolvedValue({ - ok: true, - json: async () => mockResponse, - }); - - const hasPr = 'true'; - const githubDetails = { - eventName: 'push', - baseRef: '', - githubRefName: 'feature-branch', - githubToken: GITHUB_TOKEN, - repo: FULL_REPO_NAME, - }; - - const baseline = await normalizeBaseline(hasPr, inputBaseline, githubDetails); - - expect(baseline).toBe('origin/staging'); - }); - - it('falls back to input baseline when base branch cannot be fetched', async () => { - fetch.mockResolvedValue({ - ok: false, - }); - - const hasPr = 'true'; - const githubDetails = { - eventName: 'push', - baseRef: '', - githubRefName: 'feature-branch', - githubToken: GITHUB_TOKEN, - repo: FULL_REPO_NAME, - }; - - const baseline = await normalizeBaseline(hasPr, inputBaseline, githubDetails); - - expect(baseline).toBe('origin/main'); - }); -}); - -describe('constructSemgrepCommand', () => { - it('constructs args correctly for diff scan mode', () => { - const baseline = 'origin/main'; - const semgrepConfig = { - scanMode: 'diff', - rules: 'p/rule1 p/rule2', - targets: './src,./lib', - failLevel: 'warning', - extraArgs: '--other arg1 --another arg2', - }; - const cmd = constructSemgrepCommand(baseline, semgrepConfig, 'temp-results.json'); - - expect(cmd).toEqual( - 'semgrep --config p/rule1 --config p/rule2 --severity WARNING --json --output temp-results.json --baseline-commit origin/main --other arg1 --another arg2 ./src ./lib' - ); - }); - - it('constructs args correctly for full scan mode', () => { - const baseline = 'origin/main'; - const semgrepConfig = { - scanMode: 'full', - rules: 'p/rule1', - targets: '', - failLevel: 'error', - extraArgs: '', - }; - - const cmd = constructSemgrepCommand(baseline, semgrepConfig, 'temp-results.json'); - - expect(cmd).toEqual( - 'semgrep --config p/rule1 --severity ERROR --json --output temp-results.json' - ); - }); -}); - -describe('stageResultsForReviewdog', () => { - it('stages results file when it exists', () => { - const fakeInputFileName = 'fake-results.json'; - - jest.spyOn(fs, 'readFileSync').mockImplementation((fileName, encoding) => { - if (fileName === fakeInputFileName) { - return JSON.stringify(exampleSemgrepOutput); - } - }); - - let writtenContent = ''; - jest.spyOn(fs, 'writeFileSync').mockImplementation((fileName, data) => { - if (fileName === REVIEWDOG_INPUT_FILE_NAME) { - writtenContent = data; - } - }); - - stageResultsForReviewdog(fakeInputFileName); - - expect(writtenContent).toContain('E:src/error.js:111 This is an error message'); - expect(writtenContent).toContain('W:src/warning1.js:222 This is a warning message #1'); - expect(writtenContent).toContain('I:src/info.js:333 This is an info message'); - expect(writtenContent).toContain('W:src/warning2.js:444 This is a warning message #2'); - - jest.restoreAllMocks(); - }); -}); - -describe('getSemgrepMetrics', () => { - it('correctly parses semgrep JSON output', () => { - const fakeInputFileName = 'fake-results.json'; - - jest.spyOn(fs, 'readFileSync').mockImplementation((fileName, encoding) => { - if (fileName === fakeInputFileName) { - return JSON.stringify(exampleSemgrepOutput); - } - }); - - const metrics = getSemgrepMetrics(fakeInputFileName); - - expect(metrics.totalFindings).toBe(4); - expect(metrics.numErrors).toBe(1); - expect(metrics.numWarnings).toBe(2); - expect(metrics.numInfo).toBe(1); - - jest.restoreAllMocks(); - }); - - it('handles empty results', () => { - const fakeInputFileName = 'fake-results.json'; - - jest.spyOn(fs, 'readFileSync').mockImplementation((fileName, encoding) => { - if (fileName === fakeInputFileName) { - return JSON.stringify(emptySemgrepOutput); - } - }); - - const metrics = getSemgrepMetrics(fakeInputFileName); - - expect(metrics.totalFindings).toBe(0); - expect(metrics.numErrors).toBe(0); - expect(metrics.numWarnings).toBe(0); - expect(metrics.numInfo).toBe(0); - - jest.restoreAllMocks(); - }); -}); - -describe('writeFindingsMarkdown', () => { - it('writes markdown correctly', () => { - const metrics = { - totalFindings: 6, - numErrors: 1, - numWarnings: 2, - numInfo: 3, - }; - - const markdown = writeFindingsMarkdown(metrics); - - expect(markdown).toContain('### Scan Findings\n'); - expect(markdown).toContain('| Total | Errors | Warnings | Info |\n'); - expect(markdown).toContain('| 6 | 1 | 2 | 3 |'); - }); -}); - -describe('writeConfigMarkdown', () => { - it('writes config markdown correctly', () => { - const config = { - rules: 'p/rule1 p/rule2', - targets: './src ./lib', - scanMode: 'diff', - failLevel: 'warning', - extraArgs: '--json', - }; - const baseline = 'origin/main'; - - const markdown = writeConfigMarkdown(baseline, config); - - expect(markdown).toContain('### Scan Config\n'); - expect(markdown).toContain(`- **Rules**: \`${config.rules}\`\n`); - expect(markdown).toContain(`- **Targets**: \`${config.targets}\`\n`); - expect(markdown).toContain(`- **Scan mode**: \`${config.scanMode}\`\n`); - expect(markdown).toContain(`- **Baseline**: \`${baseline}\`\n`); - expect(markdown).toContain(`- **Fail level**: \`${config.failLevel}\`\n`); - expect(markdown).toContain(`- **Extra args**: \`${config.extraArgs}\``); - }); -}); - -describe('evaluateScanStatus', () => { - it('returns failed for error severity with errors', () => { - const metrics = { numErrors: 1 }; - const status = evaluateScanStatus('error', metrics); - expect(status).toBe('failure'); - }); - it('returns failed for warning severity with warnings', () => { - const metrics = { numErrors: 0, numWarnings: 1 }; - const status = evaluateScanStatus('warning', metrics); - expect(status).toBe('failure'); - }); - it('returns failed for info severity with any findings', () => { - const metrics = { numErrors: 0, numWarnings: 0, numInfo: 1 }; - const status = evaluateScanStatus('info', metrics); - expect(status).toBe('failure'); - }); - it('returns passed when no findings exceed severity', () => { - const metrics = { numErrors: 0, numWarnings: 1 }; - const status = evaluateScanStatus('error', metrics); - expect(status).toBe('success'); - }); -}); diff --git a/scripts/internal-ci/get-version-tags/index.integration.test.js b/scripts/internal-ci/get-version-tags/index.integration.test.js index 4bd55f6..7c208b8 100644 --- a/scripts/internal-ci/get-version-tags/index.integration.test.js +++ b/scripts/internal-ci/get-version-tags/index.integration.test.js @@ -1,5 +1,5 @@ const { versionLabelPrefix, untrackedLabel } = require('../validate-version-labels/.'); -const { parseGithubOutput } = require('../../utils/test-helpers'); +const { parseGithubOutput } = require('../../internal-utils/test-helpers'); describe('get-version-tags main module integration', () => { const fs = require('fs'); diff --git a/scripts/internal-ci/validate-version-labels/index.integration.test.js b/scripts/internal-ci/validate-version-labels/index.integration.test.js index 6cb8acf..6d5f1ff 100644 --- a/scripts/internal-ci/validate-version-labels/index.integration.test.js +++ b/scripts/internal-ci/validate-version-labels/index.integration.test.js @@ -1,5 +1,5 @@ const { versionLabelPrefix, untrackedLabel } = require('.'); -const { parseGithubOutput } = require('../../utils/test-helpers'); +const { parseGithubOutput } = require('../../internal-utils/test-helpers'); describe('validate-version-labels main module integration', () => { const fs = require('fs'); diff --git a/scripts/utils/test-helpers.js b/scripts/internal-utils/test-helpers.js similarity index 100% rename from scripts/utils/test-helpers.js rename to scripts/internal-utils/test-helpers.js