Skip to content
Open
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
2 changes: 1 addition & 1 deletion .github/actions/TEMPLATE/README_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Basic usage example:
```yaml
- name: Name for step
id: <step-id>
uses: ./.github/actions/<action-name>
uses: OpenSesame/core-github-actions/.github/actions/<action-name>@actions/<action-name>/vX.Y.Z
with:
<input-name>: <value>
```
Expand Down
2 changes: 1 addition & 1 deletion .github/actions/pr-open-check/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
Expand Down
14 changes: 14 additions & 0 deletions .github/actions/run-semgrep/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
113 changes: 113 additions & 0 deletions .github/actions/run-semgrep/README.md
Original file line number Diff line number Diff line change
@@ -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.
- `https` — standard Node.js module for API requests included in default action runners
- `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.
35 changes: 35 additions & 0 deletions .github/actions/run-semgrep/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: 'Run Semgrep'
description: 'Run a Semgrep scan and output results for reviewdog and future steps'
outputs:
normalizedBaseline:
description: 'The resolved baseline ref used for reviewdog annotations'
value: ${{ steps.run-semgrep.outputs.normalizedBaseline }}
scanSummary:
description: 'Markdown summary of the Semgrep scan results'
value: ${{ steps.run-semgrep.outputs.scanSummary }}
configSummary:
description: 'Markdown summary of the Semgrep configuration used'
value: ${{ steps.run-semgrep.outputs.configSummary }}
scanStatus:
description: 'success/failure based on findings and input fail level'
value: ${{ steps.run-semgrep.outputs.scanStatus }}
totalFindings:
description: 'Total number of findings from the Semgrep scan'
value: ${{ steps.run-semgrep.outputs.totalFindings }}
numErrors:
description: 'Number of findings at error severity level'
value: ${{ steps.run-semgrep.outputs.numErrors }}
numWarnings:
description: 'Number of findings at warning severity level'
value: ${{ steps.run-semgrep.outputs.numWarnings }}
numInfos:
description: 'Number of findings at info severity level'
value: ${{ steps.run-semgrep.outputs.numInfos }}

runs:
using: composite
steps:
- name: Run Semgrep Scan
id: run-semgrep
shell: bash
run: node ${{ github.action_path }}/run-semgrep.js
Original file line number Diff line number Diff line change
@@ -1,61 +1,51 @@
/*
* 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 https = require('https');

const SEMGREP_RESULTS_FILE_NAME = 'semgrep_results.json';
const REVIEWDOG_INPUT_FILE_NAME = 'reviewdog_input.txt';

async function getPrBaseBranch(owner, repo, branch, token) {
function getPrBaseBranch(owner, repo, branch, token) {
// Use GitHub API to find open PR for the branch and get its base branch
const url = `https://api.github.com/repos/${owner}/${repo}/pulls?state=open&head=${owner}:${branch}`;
const res = await fetch(url, {
const url = `/repos/${owner}/${repo}/pulls?state=open&head=${owner}:${branch}`;
const options = {
hostname: 'api.github.com',
path: url,
method: 'GET',
headers: {
Authorization: `Bearer ${token}`,
Accept: 'application/vnd.github.v3+json',
'User-Agent': 'normalize-push-baseline-script',
},
};

return new Promise(resolve => {
const req = https.request(options, res => {
let data = '';
res.on('data', chunk => {
data += chunk;
});
res.on('end', () => {
if (res.statusCode < 200 || res.statusCode >= 300) {
return resolve(null);
}
try {
const prs = JSON.parse(data);
if (Array.isArray(prs) && prs.length > 0 && prs[0].base && prs[0].base.ref) {
resolve(prs[0].base.ref);
} else {
resolve(null);
}
} catch (_e) {
resolve(null);
}
});
});
req.on('error', _err => {
resolve(null);
});
req.end();
});
if (!res.ok) return null;
const prs = await res.json();
if (prs.length > 0 && prs[0].base && prs[0].base.ref) {
return prs[0].base.ref;
}
return null;
}

/* Normalize the baseline ref for push events in GitHub Actions.
Expand Down Expand Up @@ -327,6 +317,13 @@ if (require.main === module) {
});
}

function validateEnvVar(name) {
if (!process.env[name]) {
console.error(`::error::Environment variable ${name} is required`);
process.exit(1);
}
}

module.exports = {
main,
getPrBaseBranch,
Expand All @@ -338,6 +335,7 @@ module.exports = {
writeFindingsMarkdown,
writeConfigMarkdown,
evaluateScanStatus,
validateEnvVar,
SEMGREP_RESULTS_FILE_NAME,
REVIEWDOG_INPUT_FILE_NAME,
};
Loading