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/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/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..6808ba9 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 + +### Changed + +- 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 ### Added diff --git a/.github/workflows/run_semgrep_scan.yml b/.github/workflows/run_semgrep_scan.yml index 0d54de8..60b6b79 100644 --- a/.github/workflows/run_semgrep_scan.yml +++ b/.github/workflows/run_semgrep_scan.yml @@ -92,7 +92,7 @@ jobs: normalized_baseline: ${{ steps.semgrep.outputs.normalizedBaseline }} steps: - - name: Checkout code + - name: Checkout Calling Repo uses: actions/checkout@v4 with: ref: ${{ inputs.commit_identifier }} @@ -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 }} @@ -126,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 }} @@ -135,7 +136,6 @@ jobs: SEMGREP_TARGETS: ${{ inputs.semgrep_targets }} FAIL_LEVEL: ${{ inputs.fail_severity }} EXTRA_ARGS: ${{ inputs.extra_args }} - run: node scripts/gha-lib/run-semgrep.js - name: Upload Artifact if: ${{ steps.semgrep.outputs.totalFindings > 0 }} @@ -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 }} 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