NodeJS Security

SAST findings with CodeQL

Vulnerable package findings with Trivy

Unknown or forbidden license findings

Software Bill of Materials (SBOM) in SPDX format
This workflow uses a CodeQL job. GitHub does not provide a method to break the build with CodeQL, it only reports findings to the Security tab.
It is highly recommended to add a protection rule to your repository which will prevent merging of pull requests that introduce new CodeQL findings of significant severity.
Purpose
This workflow is for node projects that need to scan dependencies for vulnerabilities and licenses that are not allowed by OSPO.
It will also run CodeQL as a SAST tool and produce a Software Bill of Materials (SBOM).
All jobs are integrated with GitHub Advanced Security (GHAS) and will produce findings in the Security tab of your repository.
Usage
Minimal Example
Below is an example of the minimal configuration required to run the workflow. It's based on sensible defaults but will not satisfy all use cases.
To see how to customise the job(s) to work for your case, refer to inputs, secrets and the Additional examples.
jobs:
node-security:
uses: erzz/toolbox/.github/workflows/node-security.yml@v1
# no mandatory inputs
# no mandatory secrets
Inputs
| Input | Type | Description | Required | Default |
|---|---|---|---|---|
| node-version | string | To version of NodeJS to use | false | 20 |
| install-command | string | Used to override the default command to install your dependencies e.g. yarn install | false | yarn install --silent |
| path | string | Relative path from project root to your node application's source files | false | . |
| codeql-enable | boolean | To disable the CodeQL job set to false | false | true |
| codeql-language | string | One of `javascript` or `typescript`. Determines CodeQL configuration | false | javascript |
| deps-enable | boolean | To disable the dependency scanning job set to false | false | true |
| deps-severity | string | The minimum severity to report | false | MEDIUM,HIGH,CRITICAL |
| deps-timeout | string | The maximum time to wait for the scan to complete | false | 10m |
| licence-enable | boolean | To disable the licence scanning job set to false | false | true |
| licence-default | boolean | To use your own licence configuration set to false and provide a licence-config | false | true |
| licence-config | string | The relative path to your own trivy config file for license scanning e.g. `licenses.yml` | false | licenses.yml |
| sbom-enable | boolean | To disable the SBOM job set to false | false | true |
| artifact-prefix | string | A prefix to apply to the name of the artifacts to upload (useful for matrix builds) | false | null |
| trivy-version | string | Override the version of Trivy (deps/license/sbom) to utilise | false | null |
| trivy-db-cache | boolean | Enable caching of the Trivy DB (deps/license/sbom) | false | true |
Secrets
| Input | Description | Required |
|---|---|---|
| auth-token | Arbitrary token to be exposed as environment variable in the format VARIABLE=value | false |
| auth-token-2 | Arbitrary token to be exposed as environment variable in the format VARIABLE=value | false |
Outputs
| Output | Description | Value |
|---|---|---|
| codeql-artifact-id | The unique id of the codeql SARIF artifact | ${{ jobs.sast.outputs.artifact-id }} |
| codeql-artifact-url | The download URL of the codeql SARIF artifact | ${{ jobs.sast.outputs.artifact-url }} |
| ref-slug | A URL sanitized version of the github ref | ${{ jobs.sast.outputs.ref-slug }} |
| short-sha | Captures the short SHA for use in this or later workflow jobs | ${{ jobs.sast.outputs.short-sha }} |
Configuration Notes
The workflow itself will not provide the scripts and dependencies. It simply executes your commands and provide artifacts if the appropriate dependencies are installed and configured.
Build authentication
Secrets are provided for build-time authentication with private package registries such as Artifactory, NPM etc.
There are options to provide an auth-token and/or auth-token-2 where the environment variable
they are exposed as is configurable.
For example you could expose auth-token: NPM_TOKEN=${{ secrets.SOME_SECRET }} or
auth-token: ARTIFACTORY_AUTH_TOKEN=${{ secrets.SOME_SECRET }} depending on what your build process
requires.
Additional Examples
Disable specific jobs
For example, to disable the licence scanning job:
node-security:
uses: erzz/toolbox/.github/workflows/node-security.yml@v1
with:
licence-enable: false
Custom commands and auth tokens
Most relevant commands are customisable, even with multiple commands using the | syntax. You can
pass in any environment variables you need to the commands for authentication with private package
registries.
node-security:
uses: erzz/toolbox/.github/workflows/node-security.yml@v1
with:
install-command: |
cp .env.example .env
npm ci
secrets:
auth-token: "NPM_TOKEN=${{ secrets.GH_NPM_AUTH_TOKEN }}"
auth-token-2: "ARTIFACTORY_AUTH_TOKEN=${{ secrets.ARTIFACTORY_AUTH_TOKEN }}"
Workflow details
Source code
name: NodeJS Security
permissions:
actions: read
checks: write
contents: read
id-token: write
security-events: write
pull-requests: write
env:
TRIVY_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-db:2
TRIVY_JAVA_DB_REPOSITORY: public.ecr.aws/aquasecurity/trivy-java-db:1
TRIVY_CACHE_DIR: ${{ github.workspace }}/.cache/trivy
on:
workflow_call:
inputs:
node-version:
type: string
description: To version of NodeJS to use
required: false
default: "20"
install-command:
required: false
type: string
description: Used to override the default command to install your dependencies
e.g. yarn install
default: yarn install --silent
path:
required: false
type: string
description: Relative path from project root to your node application's source files
default: .
codeql-enable:
required: false
type: boolean
description: To disable the CodeQL job set to false
default: true
codeql-language:
required: false
type: string
description: One of `javascript` or `typescript`. Determines CodeQL configuration
default: javascript
deps-enable:
required: false
type: boolean
description: To disable the dependency scanning job set to false
default: true
deps-severity:
required: false
type: string
description: The minimum severity to report
default: MEDIUM,HIGH,CRITICAL
deps-timeout:
required: false
type: string
description: The maximum time to wait for the scan to complete
default: 10m
licence-enable:
required: false
type: boolean
description: To disable the licence scanning job set to false
default: true
licence-default:
required: false
type: boolean
description: To use your own licence configuration set to false and provide a
licence-config
default: true
licence-config:
required: false
type: string
description: The relative path to your own trivy config file for license
scanning e.g. `licenses.yml`
default: licenses.yml
sbom-enable:
required: false
type: boolean
description: To disable the SBOM job set to false
default: true
artifact-prefix:
type: string
description: A prefix to apply to the name of the artifacts to upload (useful
for matrix builds)
required: false
trivy-version:
required: false
type: string
description: Override the version of Trivy (deps/license/sbom) to utilise
trivy-db-cache:
required: false
type: boolean
description: Enable caching of the Trivy DB (deps/license/sbom)
default: true
secrets:
auth-token:
required: false
description: Arbitrary token to be exposed as environment variable in the format
VARIABLE=value
auth-token-2:
required: false
description: Arbitrary token to be exposed as environment variable in the format
VARIABLE=value
outputs:
codeql-artifact-id:
description: The unique id of the codeql SARIF artifact
value: ${{ jobs.sast.outputs.artifact-id }}
codeql-artifact-url:
description: The download URL of the codeql SARIF artifact
value: ${{ jobs.sast.outputs.artifact-url }}
ref-slug:
description: A URL sanitized version of the github ref
value: ${{ jobs.sast.outputs.ref-slug }}
short-sha:
description: Captures the short SHA for use in this or later workflow jobs
value: ${{ jobs.sast.outputs.short-sha }}
jobs:
sast:
name: CodeQL SAST
if: ${{ inputs.codeql-enable }}
runs-on: ubuntu-24.04
outputs:
artifact-id: ${{ steps.upload.outputs.artifact-id }}
artifact-url: ${{ steps.upload.outputs.artifact-url }}
steps:
- name: Checkout Source
uses: actions/checkout@v4
- name: Setup NodeJS
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Export auth token 1
env:
TOKEN: ${{ secrets.auth-token }}
if: ${{ env.TOKEN != '' }}
run: |
echo "::add-mask::${TOKEN#*=}"
echo "${{ env.TOKEN }}" >> $GITHUB_ENV
- name: Export auth token 2
env:
TOKEN: ${{ secrets.auth-token-2 }}
if: ${{ env.TOKEN != '' }}
run: |
echo "::add-mask::${TOKEN#*=}"
echo "${{ env.TOKEN }}" >> $GITHUB_ENV
- name: Run install command
working-directory: ${{ inputs.path }}
run: |
${{ inputs.install-command }}
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ inputs.codeql-language }}
- name: Perform CodeQL Analysis
id: codeql
uses: github/codeql-action/analyze@v3
- name: Upload Reports
id: upload
uses: actions/upload-artifact@v4
if: always()
with:
name: ${{ inputs.artifact-prefix }}CodeQL SARIF Report
path: ${{ steps.codeql.outputs.sarif-output }}
deps:
name: Dependency Scan
if: ${{ inputs.deps-enable }}
runs-on: ubuntu-24.04
steps:
- name: Checkout Source
uses: actions/checkout@v4
- name: Install Trivy
uses: aquasecurity/setup-[email protected]
with:
version: ${{ inputs.trivy-version }}
cache: true
- name: Create cache directory
run: |
echo "Creating Trivy cache directory ${{ env.TRIVY_CACHE_DIR }}..."
mkdir -p ${{ env.TRIVY_CACHE_DIR }}
- name: Get current date
if: ${{ inputs.trivy-db-cache }}
id: date
shell: bash
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Restore DB from cache
if: ${{ inputs.trivy-db-cache }}
uses: actions/cache@v4
with:
path: ${{ env.TRIVY_CACHE_DIR }}
key: cache-trivy-${{ steps.date.outputs.date }}
restore-keys: cache-trivy-
- name: Execute Scan
run: |
trivy --version
tree ${{ env.TRIVY_CACHE_DIR }}
trivy repo \
--format json \
--output dependency-scan.json \
--ignore-unfixed \
--severity ${{ inputs.deps-severity }} \
--exit-code 0 \
--timeout ${{ inputs.deps-timeout }} \
--cache-dir ${{ env.TRIVY_CACHE_DIR }} \
${{ inputs.path }}
- name: Results
run: >
if [ $(jq '[.Results[] | .Vulnerabilities | length] | add'
dependency-scan.json) -gt 0 ]; then
echo "❌ Vulnerabilities found"
trivy convert \
--format table \
dependency-scan.json
exit 1
else
echo "✨ No vulnerabilities found"
exit 0
fi
- name: Generate SARIF report
if: always()
run: |
trivy convert \
--format sarif \
--output dependency-scan.sarif \
dependency-scan.json
- name: Upload artifacts
uses: actions/upload-artifact@v4
if: always()
with:
name: ${{ inputs.artifact-prefix }}Dependency SARIF Report
path: dependency-scan.sarif
- name: Upload results to GitHub Security
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: dependency-scan.sarif
license-scan:
name: License Scan
if: ${{ inputs.licence-enable }}
runs-on: ubuntu-24.04
steps:
- name: Checkout the code
uses: actions/checkout@v4
- name: Setup NodeJS
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Get Configuration
if: ${{ inputs.licence-default }}
run: >
echo "Fetching default configuration from erzz/toolbox/configs..."
curl --header 'authorization: Bearer ${{ secrets.GITHUB_TOKEN }}' \
--header 'Accept: application/vnd.github.v3.raw' \
--location https://api.github.com/repos/erzz/toolbox/contents/configs/license-scan/licenses.yml \
--fail \
--output ${{ inputs.licence-config }} || exit 1
cat ${{ inputs.licence-config }}
- name: Export auth token 1
env:
TOKEN: ${{ secrets.auth-token }}
if: ${{ env.TOKEN != '' }}
run: |
echo "::add-mask::${TOKEN#*=}"
echo "${{ env.TOKEN }}" >> $GITHUB_ENV
- name: Export auth token 2
env:
TOKEN: ${{ secrets.auth-token-2 }}
if: ${{ env.TOKEN != '' }}
run: |
echo "::add-mask::${TOKEN#*=}"
echo "${{ env.TOKEN }}" >> $GITHUB_ENV
- name: Run install command
working-directory: ${{ inputs.path }}
run: |
${{ inputs.install-command }}
- name: Install Trivy
uses: aquasecurity/setup-[email protected]
with:
version: ${{ inputs.trivy-version }}
cache: true
- name: Create cache directory
run: |
echo "Creating Trivy cache directory ${{ env.TRIVY_CACHE_DIR }}..."
mkdir -p ${{ env.TRIVY_CACHE_DIR }}
- name: Get current date
if: ${{ inputs.trivy-db-cache }}
id: date
shell: bash
run: echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT
- name: Restore DB from cache
if: ${{ inputs.trivy-db-cache }}
uses: actions/cache@v4
with:
path: ${{ env.TRIVY_CACHE_DIR }}
key: cache-trivy-${{ steps.date.outputs.date }}
restore-keys: cache-trivy-
- name: Execute Scan
run: |
if [ ! -f .trivyignore ]; then
echo "Creating .trivyignore..."
touch .trivyignore
fi
trivy --version
tree ${{ env.TRIVY_CACHE_DIR }}
trivy rootfs \
--format json \
--output license-results.json \
--exit-code 0 \
--config ${{ inputs.licence-config }} \
--cache-dir ${{ env.TRIVY_CACHE_DIR }} \
${{ inputs.path }}
- name: Results
run: >
if [ $(jq '[.Results[] | .Licenses | length] | add'
license-results.json) -gt 0 ]; then
echo "❌ Possible license issues found"
trivy convert \
--format table \
license-results.json
exit 1
else
echo "✨ No license issues found"
exit 0
fi
- name: Generate SARIF report
if: always()
run: |
trivy convert \
--format sarif \
--output license-results.sarif \
license-results.json
- name: Upload artifacts
uses: actions/upload-artifact@v4
if: always()
with:
name: ${{ inputs.artifact-prefix }}Licences SARIF Report
path: license-results.sarif
- name: Upload results to GitHub Security
if: always()
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: license-results.sarif
sbom:
name: SBOM
runs-on: ubuntu-24.04
steps:
- name: Checkout the code
uses: actions/checkout@v4
- name: Setup NodeJS
uses: actions/setup-node@v4
with:
node-version: ${{ inputs.node-version }}
- name: Export auth token 1
env:
TOKEN: ${{ secrets.auth-token }}
if: ${{ env.TOKEN != '' }}
run: |
echo "::add-mask::${TOKEN#*=}"
echo "${{ env.TOKEN }}" >> $GITHUB_ENV
- name: Export auth token 2
env:
TOKEN: ${{ secrets.auth-token-2 }}
if: ${{ env.TOKEN != '' }}
run: |
echo "::add-mask::${TOKEN#*=}"
echo "${{ env.TOKEN }}" >> $GITHUB_ENV
- name: Run install command
working-directory: ${{ inputs.path }}
run: |
${{ inputs.install-command }}
- name: Install Trivy
uses: aquasecurity/setup-[email protected]
with:
version: ${{ inputs.trivy-version }}
cache: true
- name: Create cache directory
run: |
echo "Creating Trivy cache directory ${{ env.TRIVY_CACHE_DIR }}..."
mkdir -p ${{ env.TRIVY_CACHE_DIR }}
- name: Generate SBOM
run: |
trivy --version
trivy repo \
--format spdx-json \
--output sbom.spdx.json \
${{ inputs.path }}
- name: Upload artifacts
uses: actions/upload-artifact@v4
if: always()
with:
name: ${{ inputs.artifact-prefix }}SBOM
path: sbom.spdx.json
Job definitions
| Step | Uses | Conditional |
|---|---|---|
| Checkout Source | actions/checkout@v4 | false |
| Setup NodeJS | actions/setup-node@v4 | false |
| Export auth token 1 | script | true |
| Export auth token 2 | script | true |
| Run install command | script | false |
| Initialize CodeQL | github/codeql-action/init@v3 | false |
| Perform CodeQL Analysis | github/codeql-action/analyze@v3 | false |
| Upload Reports | actions/upload-artifact@v4 | true |
| Step | Uses | Conditional |
|---|---|---|
| Checkout Source | actions/checkout@v4 | false |
| Install Trivy | aquasecurity/[email protected] | false |
| Create cache directory | script | false |
| Get current date | script | true |
| Restore DB from cache | actions/cache@v4 | true |
| Execute Scan | script | false |
| Results | script | false |
| Generate SARIF report | script | true |
| Upload artifacts | actions/upload-artifact@v4 | true |
| Upload results to GitHub Security | github/codeql-action/upload-sarif@v3 | true |
| Step | Uses | Conditional |
|---|---|---|
| Checkout the code | actions/checkout@v4 | false |
| Setup NodeJS | actions/setup-node@v4 | false |
| Get Configuration | script | true |
| Export auth token 1 | script | true |
| Export auth token 2 | script | true |
| Run install command | script | false |
| Install Trivy | aquasecurity/[email protected] | false |
| Create cache directory | script | false |
| Get current date | script | true |
| Restore DB from cache | actions/cache@v4 | true |
| Execute Scan | script | false |
| Results | script | false |
| Generate SARIF report | script | true |
| Upload artifacts | actions/upload-artifact@v4 | true |
| Upload results to GitHub Security | github/codeql-action/upload-sarif@v3 | true |
| Step | Uses | Conditional |
|---|---|---|
| Checkout the code | actions/checkout@v4 | false |
| Setup NodeJS | actions/setup-node@v4 | false |
| Export auth token 1 | script | true |
| Export auth token 2 | script | true |
| Run install command | script | false |
| Install Trivy | aquasecurity/[email protected] | false |
| Create cache directory | script | false |
| Generate SBOM | script | false |
| Upload artifacts | actions/upload-artifact@v4 | true |
Configuration Files
They are provided to get you started with sensible defaults. It's OK to replace them in order to tweak for your own preferences if required.