Skip to main content

NodeJS Security

Status: production
This workflow is stable and widely used in production environments.
Security and software composition analysis for NodeJS projects
SAST findings with CodeQL

SAST findings with CodeQL

Vulnerable package findings with Trivy

Vulnerable package findings with Trivy

Unknown or forbidden license findings

Unknown or forbidden license findings

Software Bill of Materials (SBOM) in SPDX format

Software Bill of Materials (SBOM) in SPDX format

This workflow requires GitHub Advanced Security
You can find instructions to enable it here
CodeQL does not 'break the build'

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.

Bare minimum code to insert in your workflow
jobs:
node-security:
uses: erzz/toolbox/.github/workflows/node-security.yml@v1
# no mandatory inputs
# no mandatory secrets

Inputs

InputTypeDescriptionRequiredDefault
node-versionstringTo version of NodeJS to usefalse20
install-commandstringUsed to override the default command to install your dependencies e.g. yarn installfalseyarn install --silent
pathstringRelative path from project root to your node application's source filesfalse.
codeql-enablebooleanTo disable the CodeQL job set to falsefalsetrue
codeql-languagestringOne of `javascript` or `typescript`. Determines CodeQL configurationfalsejavascript
deps-enablebooleanTo disable the dependency scanning job set to falsefalsetrue
deps-severitystringThe minimum severity to reportfalseMEDIUM,HIGH,CRITICAL
deps-timeoutstringThe maximum time to wait for the scan to completefalse10m
licence-enablebooleanTo disable the licence scanning job set to falsefalsetrue
licence-defaultbooleanTo use your own licence configuration set to false and provide a licence-configfalsetrue
licence-configstringThe relative path to your own trivy config file for license scanning e.g. `licenses.yml`falselicenses.yml
sbom-enablebooleanTo disable the SBOM job set to falsefalsetrue
artifact-prefixstringA prefix to apply to the name of the artifacts to upload (useful for matrix builds)falsenull
trivy-versionstringOverride the version of Trivy (deps/license/sbom) to utilisefalsenull
trivy-db-cachebooleanEnable caching of the Trivy DB (deps/license/sbom)falsetrue

Secrets

InputDescriptionRequired
auth-tokenArbitrary token to be exposed as environment variable in the format VARIABLE=valuefalse
auth-token-2Arbitrary token to be exposed as environment variable in the format VARIABLE=valuefalse

Outputs

OutputDescriptionValue
codeql-artifact-idThe unique id of the codeql SARIF artifact${{ jobs.sast.outputs.artifact-id }}
codeql-artifact-urlThe download URL of the codeql SARIF artifact${{ jobs.sast.outputs.artifact-url }}
ref-slugA URL sanitized version of the github ref${{ jobs.sast.outputs.ref-slug }}
short-shaCaptures the short SHA for use in this or later workflow jobs${{ jobs.sast.outputs.short-sha }}

Configuration Notes

info

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

View on Github
.github/workflows/node-security.yml
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

StepUsesConditional
Checkout Sourceactions/checkout@v4false
Setup NodeJSactions/setup-node@v4false
Export auth token 1scripttrue
Export auth token 2scripttrue
Run install commandscriptfalse
Initialize CodeQLgithub/codeql-action/init@v3false
Perform CodeQL Analysisgithub/codeql-action/analyze@v3false
Upload Reportsactions/upload-artifact@v4true

StepUsesConditional
Checkout Sourceactions/checkout@v4false
Install Trivyaquasecurity/[email protected]false
Create cache directoryscriptfalse
Get current datescripttrue
Restore DB from cacheactions/cache@v4true
Execute Scanscriptfalse
Resultsscriptfalse
Generate SARIF reportscripttrue
Upload artifactsactions/upload-artifact@v4true
Upload results to GitHub Securitygithub/codeql-action/upload-sarif@v3true

StepUsesConditional
Checkout the codeactions/checkout@v4false
Setup NodeJSactions/setup-node@v4false
Get Configurationscripttrue
Export auth token 1scripttrue
Export auth token 2scripttrue
Run install commandscriptfalse
Install Trivyaquasecurity/[email protected]false
Create cache directoryscriptfalse
Get current datescripttrue
Restore DB from cacheactions/cache@v4true
Execute Scanscriptfalse
Resultsscriptfalse
Generate SARIF reportscripttrue
Upload artifactsactions/upload-artifact@v4true
Upload results to GitHub Securitygithub/codeql-action/upload-sarif@v3true

StepUsesConditional
Checkout the codeactions/checkout@v4false
Setup NodeJSactions/setup-node@v4false
Export auth token 1scripttrue
Export auth token 2scripttrue
Run install commandscriptfalse
Install Trivyaquasecurity/[email protected]false
Create cache directoryscriptfalse
Generate SBOMscriptfalse
Upload artifactsactions/upload-artifact@v4true

Configuration Files

This workflow provides some default 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.

Diagram