Skip to content

Commit f5d6c40

Browse files
committed
Add rules related to npm publish
1 parent 17ab87d commit f5d6c40

File tree

12 files changed

+260
-0
lines changed

12 files changed

+260
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
## Overview
2+
3+
The `npm publish` command does not include the `--provenance` flag. Provenance attestation cryptographically links the published package to a specific source commit and workflow run, allowing consumers to verify where and how the package was built.
4+
5+
## Recommendation
6+
7+
Add `--provenance` to the `npm publish` command. This requires the workflow to have `id-token: write` permission and to run in a GitHub Actions environment.
8+
9+
## Example
10+
11+
### Incorrect Usage
12+
13+
```yaml
14+
- name: Publish
15+
run: npm publish
16+
```
17+
18+
### Correct Usage
19+
20+
```yaml
21+
permissions:
22+
id-token: write
23+
24+
- name: Publish
25+
run: npm publish --provenance
26+
```
27+
28+
## References
29+
30+
- npm Docs: [Generating provenance statements](https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions).
31+
- GitHub Blog: [Introducing npm package provenance](https://github.blog/security/supply-chain-security/introducing-npm-package-provenance/).
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* @name npm publish missing --provenance flag
3+
* @description The npm publish command does not include '--provenance'. Provenance attestation
4+
* cryptographically links the published package to a specific source commit and
5+
* workflow run.
6+
* @kind problem
7+
* @problem.severity warning
8+
* @security-severity 5.0
9+
* @precision high
10+
* @id actions/missing-provenance-flag
11+
* @tags actions
12+
* security
13+
* supply-chain
14+
* external/cwe/cwe-353
15+
*/
16+
17+
import actions
18+
19+
from Run run, string command
20+
where
21+
command = run.getScript().getACommand() and
22+
command.regexpMatch("(?i).*\\bnpm\\s+publish\\b.*") and
23+
not command.regexpMatch("(?i).*\\bnpm\\s+publish\\b.*--provenance\\b.*")
24+
select run,
25+
"npm publish command does not include '--provenance'. Add '--provenance' to cryptographically link the package to this source commit and workflow run."
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
## Overview
2+
3+
The publish step sets `NODE_AUTH_TOKEN` (or `NPM_TOKEN`) from a repository secret. This is a long-lived credential that can be stolen and used to publish malicious versions from outside the CI/CD pipeline, as demonstrated by the axios@1.14.1 supply chain attack.
4+
5+
## Recommendation
6+
7+
Remove `NODE_AUTH_TOKEN` from the publish step. Configure npm Trusted Publishing (OIDC) on npmjs.com, pointing to this repository and workflow. This eliminates the need for long-lived tokens entirely.
8+
9+
## Example
10+
11+
### Incorrect Usage
12+
13+
```yaml
14+
- name: Publish
15+
run: npm publish
16+
env:
17+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
18+
```
19+
20+
### Correct Usage
21+
22+
Use npm Trusted Publishing (OIDC) instead of a long-lived token:
23+
24+
```yaml
25+
permissions:
26+
id-token: write
27+
28+
- name: Publish
29+
run: npm publish --provenance
30+
```
31+
32+
## References
33+
34+
- npm Docs: [Generating provenance statements](https://docs.npmjs.com/generating-provenance-statements#publishing-packages-with-provenance-via-github-actions).
35+
- GitHub Blog: [Introducing npm package provenance](https://github.blog/security/supply-chain-security/introducing-npm-package-provenance/).
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* @name Long-lived npm token used in publish step
3+
* @description The publish step sets NODE_AUTH_TOKEN or NPM_TOKEN from a repository secret.
4+
* This is a long-lived credential that can be stolen and used to publish malicious
5+
* versions from outside the CI/CD pipeline.
6+
* @kind problem
7+
* @problem.severity error
8+
* @security-severity 9.0
9+
* @precision high
10+
* @id actions/npm-token-in-publish
11+
* @tags actions
12+
* security
13+
* supply-chain
14+
* external/cwe/cwe-798
15+
*/
16+
17+
import actions
18+
19+
/**
20+
* Holds if `run` is a Run step whose script contains an `npm publish` or `yarn publish` command.
21+
*/
22+
predicate isNpmPublishStep(Run run) {
23+
run.getScript().getACommand().regexpMatch("(?i).*\\bnpm\\s+publish\\b.*") or
24+
run.getScript().getACommand().regexpMatch("(?i).*\\byarn\\s+publish\\b.*")
25+
}
26+
27+
/**
28+
* Holds if `uses` is a UsesStep that calls a known npm publish action.
29+
*/
30+
predicate isNpmPublishUsesStep(UsesStep uses) {
31+
uses.getCallee().matches(["JS-DevTools/npm-publish%", "js-devtools/npm-publish%"])
32+
}
33+
34+
/**
35+
* Holds if `expr` is an expression that references a secret (e.g. `secrets.NPM_TOKEN`).
36+
*/
37+
bindingset[exprValue]
38+
predicate isSecretsReference(string exprValue) {
39+
exprValue.regexpMatch("(?i)secrets\\..*")
40+
}
41+
42+
from Step publishStep, Env env, string envVarName, Expression secretExpr
43+
where
44+
(
45+
isNpmPublishStep(publishStep) or
46+
isNpmPublishUsesStep(publishStep)
47+
) and
48+
env = publishStep.getEnv() and
49+
envVarName = ["NODE_AUTH_TOKEN", "NPM_TOKEN"] and
50+
secretExpr = env.getEnvVarExpr(envVarName) and
51+
isSecretsReference(secretExpr.getExpression())
52+
select secretExpr,
53+
"Long-lived npm token '$@' is used in a publish step. Use npm Trusted Publishing (OIDC) instead.",
54+
secretExpr, envVarName
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: npm-publish-no-provenance
2+
on:
3+
push:
4+
tags:
5+
- 'v*'
6+
jobs:
7+
publish-missing:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v4
11+
- uses: actions/setup-node@v4
12+
with:
13+
node-version: '20'
14+
registry-url: 'https://registry.npmjs.org'
15+
- run: npm publish
16+
publish-with-access:
17+
runs-on: ubuntu-latest
18+
steps:
19+
- uses: actions/checkout@v4
20+
- run: npm publish --access public
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
name: npm-publish-with-provenance
2+
on:
3+
push:
4+
tags:
5+
- 'v*'
6+
jobs:
7+
publish-correct:
8+
runs-on: ubuntu-latest
9+
permissions:
10+
id-token: write
11+
contents: read
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-node@v4
15+
with:
16+
node-version: '20'
17+
registry-url: 'https://registry.npmjs.org'
18+
- run: npm publish --provenance
19+
publish-with-access-and-provenance:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: actions/checkout@v4
23+
- run: npm publish --access public --provenance
24+
not-a-publish-step:
25+
runs-on: ubuntu-latest
26+
steps:
27+
- uses: actions/checkout@v4
28+
- run: npm install
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
| .github/workflows/npm-publish-no-provenance.yml:15:9:16:2 | Run Step | npm publish command does not include '--provenance'. Add '--provenance' to cryptographically link the package to this source commit and workflow run. |
2+
| .github/workflows/npm-publish-no-provenance.yml:20:9:20:41 | Run Step | npm publish command does not include '--provenance'. Add '--provenance' to cryptographically link the package to this source commit and workflow run. |
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Security/CWE-353/MissingProvenanceFlag.ql
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
name: npm-publish-safe
2+
on:
3+
push:
4+
tags:
5+
- 'v*'
6+
jobs:
7+
publish-oidc:
8+
runs-on: ubuntu-latest
9+
permissions:
10+
id-token: write
11+
contents: read
12+
steps:
13+
- uses: actions/checkout@v4
14+
- uses: actions/setup-node@v4
15+
with:
16+
node-version: '20'
17+
registry-url: 'https://registry.npmjs.org'
18+
- run: npm publish --provenance
19+
publish-no-secret:
20+
runs-on: ubuntu-latest
21+
steps:
22+
- uses: actions/checkout@v4
23+
- run: npm publish
24+
env:
25+
SOME_OTHER_VAR: "hello"
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
name: npm-publish-with-token
2+
on:
3+
push:
4+
tags:
5+
- 'v*'
6+
jobs:
7+
publish:
8+
runs-on: ubuntu-latest
9+
steps:
10+
- uses: actions/checkout@v4
11+
- uses: actions/setup-node@v4
12+
with:
13+
node-version: '20'
14+
registry-url: 'https://registry.npmjs.org'
15+
- run: npm publish
16+
env:
17+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
18+
- run: npm publish --provenance
19+
env:
20+
NPM_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }}
21+
publish-yarn:
22+
runs-on: ubuntu-latest
23+
steps:
24+
- uses: actions/checkout@v4
25+
- run: yarn publish
26+
env:
27+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
28+
publish-action:
29+
runs-on: ubuntu-latest
30+
steps:
31+
- uses: actions/checkout@v4
32+
- uses: JS-DevTools/npm-publish@v3
33+
env:
34+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

0 commit comments

Comments
 (0)