mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-05-19 23:33:07 +08:00
security: scope release oidc publishing
This commit is contained in:
51
.github/workflows/release.yml
vendored
51
.github/workflows/release.yml
vendored
@@ -5,13 +5,16 @@ on:
|
||||
tags: ['v*']
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Create Release
|
||||
verify:
|
||||
name: Verify Release
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
already_published: ${{ steps.npm_publish_state.outputs.already_published }}
|
||||
dist_tag: ${{ steps.npm_publish_state.outputs.dist_tag }}
|
||||
package_file: ${{ steps.pack.outputs.package_file }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -97,6 +100,42 @@ jobs:
|
||||
- For migration tips and compatibility notes, see README and CHANGELOG.
|
||||
EOF
|
||||
|
||||
- name: Pack npm artifact
|
||||
id: pack
|
||||
run: |
|
||||
npm pack --json > npm-pack.json
|
||||
PACKAGE_FILE=$(node -e "const fs = require('fs'); const data = JSON.parse(fs.readFileSync('npm-pack.json', 'utf8')); console.log(data[0].filename)")
|
||||
echo "package_file=${PACKAGE_FILE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload release artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ecc-release-artifacts
|
||||
path: |
|
||||
release_body.md
|
||||
${{ steps.pack.outputs.package_file }}
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
name: Publish Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: verify
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Download release artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: ecc-release-artifacts
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
with:
|
||||
@@ -106,7 +145,7 @@ jobs:
|
||||
make_latest: ${{ contains(github.ref_name, '-') && 'false' || 'true' }}
|
||||
|
||||
- name: Publish npm package
|
||||
if: steps.npm_publish_state.outputs.already_published != 'true'
|
||||
if: needs.verify.outputs.already_published != 'true'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npm publish --access public --provenance --tag "${{ steps.npm_publish_state.outputs.dist_tag }}"
|
||||
run: npm publish "${{ needs.verify.outputs.package_file }}" --access public --provenance --tag "${{ needs.verify.outputs.dist_tag }}"
|
||||
|
||||
51
.github/workflows/reusable-release.yml
vendored
51
.github/workflows/reusable-release.yml
vendored
@@ -28,13 +28,16 @@ on:
|
||||
default: true
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Create Release
|
||||
verify:
|
||||
name: Verify Release
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
already_published: ${{ steps.npm_publish_state.outputs.already_published }}
|
||||
dist_tag: ${{ steps.npm_publish_state.outputs.dist_tag }}
|
||||
package_file: ${{ steps.pack.outputs.package_file }}
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
@@ -114,6 +117,42 @@ jobs:
|
||||
- Claude marketplace/plugin identifier: \`everything-claude-code@everything-claude-code\`
|
||||
EOF
|
||||
|
||||
- name: Pack npm artifact
|
||||
id: pack
|
||||
run: |
|
||||
npm pack --json > npm-pack.json
|
||||
PACKAGE_FILE=$(node -e "const fs = require('fs'); const data = JSON.parse(fs.readFileSync('npm-pack.json', 'utf8')); console.log(data[0].filename)")
|
||||
echo "package_file=${PACKAGE_FILE}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Upload release artifacts
|
||||
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
||||
with:
|
||||
name: ecc-release-artifacts
|
||||
path: |
|
||||
release_body.md
|
||||
${{ steps.pack.outputs.package_file }}
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
name: Publish Release
|
||||
runs-on: ubuntu-latest
|
||||
needs: verify
|
||||
permissions:
|
||||
contents: write
|
||||
id-token: write
|
||||
|
||||
steps:
|
||||
- name: Download release artifacts
|
||||
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
||||
with:
|
||||
name: ecc-release-artifacts
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0
|
||||
with:
|
||||
node-version: '20.x'
|
||||
registry-url: 'https://registry.npmjs.org'
|
||||
|
||||
- name: Create GitHub Release
|
||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||
with:
|
||||
@@ -124,7 +163,7 @@ jobs:
|
||||
make_latest: ${{ contains(inputs.tag, '-') && 'false' || 'true' }}
|
||||
|
||||
- name: Publish npm package
|
||||
if: steps.npm_publish_state.outputs.already_published != 'true'
|
||||
if: needs.verify.outputs.already_published != 'true'
|
||||
env:
|
||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
||||
run: npm publish --access public --provenance --tag "${{ steps.npm_publish_state.outputs.dist_tag }}"
|
||||
run: npm publish "${{ needs.verify.outputs.package_file }}" --access public --provenance --tag "${{ needs.verify.outputs.dist_tag }}"
|
||||
|
||||
@@ -45,6 +45,7 @@ const NPM_AUDIT_PATTERN = /\bnpm\s+audit\b(?!\s+signatures\b)/;
|
||||
const NPM_AUDIT_SIGNATURES_PATTERN = /\bnpm\s+audit\s+signatures\b/;
|
||||
const ACTIONS_CACHE_PATTERN = /uses:\s*['"]?actions\/cache@/m;
|
||||
const ID_TOKEN_WRITE_PATTERN = /^\s*id-token:\s*write\b/m;
|
||||
const TOP_LEVEL_JOBS_PATTERN = /^jobs:\s*$/m;
|
||||
const UNSAFE_INSTALL_PATTERNS = [
|
||||
{
|
||||
pattern: /\bnpm\s+ci\b(?![^\n]*--ignore-scripts)/g,
|
||||
@@ -121,6 +122,8 @@ function extractCheckoutSteps(source) {
|
||||
function findViolations(filePath, source) {
|
||||
const violations = [];
|
||||
const checkoutSteps = extractCheckoutSteps(source);
|
||||
const jobsIndex = source.search(TOP_LEVEL_JOBS_PATTERN);
|
||||
const workflowHeader = jobsIndex >= 0 ? source.slice(0, jobsIndex) : source;
|
||||
|
||||
for (const rule of RULES) {
|
||||
if (!rule.eventPattern.test(source)) {
|
||||
@@ -175,6 +178,16 @@ function findViolations(filePath, source) {
|
||||
|
||||
}
|
||||
|
||||
if (ID_TOKEN_WRITE_PATTERN.test(workflowHeader)) {
|
||||
violations.push({
|
||||
filePath,
|
||||
event: 'workflow-scoped id-token',
|
||||
description: 'id-token: write must be scoped to a publish-only job, not the entire workflow',
|
||||
expression: 'top-level id-token: write',
|
||||
line: getLineNumber(source, source.search(ID_TOKEN_WRITE_PATTERN)),
|
||||
});
|
||||
}
|
||||
|
||||
for (const installRule of UNSAFE_INSTALL_PATTERNS) {
|
||||
for (const match of source.matchAll(installRule.pattern)) {
|
||||
violations.push({
|
||||
|
||||
@@ -244,12 +244,27 @@ function run() {
|
||||
|
||||
if (test('rejects actions/cache in workflows with id-token write', () => {
|
||||
const result = runValidator({
|
||||
'unsafe-oidc-cache.yml': `name: Unsafe\non:\n push:\npermissions:\n contents: read\n id-token: write\njobs:\n release:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/cache@v5\n with:\n path: ~/.npm\n key: cache\n`,
|
||||
'unsafe-oidc-cache.yml': `name: Unsafe\non:\n push:\npermissions:\n contents: read\njobs:\n release:\n runs-on: ubuntu-latest\n permissions:\n contents: read\n id-token: write\n steps:\n - uses: actions/cache@v5\n with:\n path: ~/.npm\n key: cache\n`,
|
||||
});
|
||||
assert.notStrictEqual(result.status, 0, 'Expected validator to fail on id-token workflow cache use');
|
||||
assert.match(result.stderr, /id-token: write must not restore or save shared dependency caches/);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects workflow-scoped id-token write', () => {
|
||||
const result = runValidator({
|
||||
'unsafe-workflow-oidc.yml': `name: Unsafe\non:\n push:\npermissions:\n contents: read\n id-token: write\njobs:\n verify:\n runs-on: ubuntu-latest\n steps:\n - run: npm ci --ignore-scripts\n`,
|
||||
});
|
||||
assert.notStrictEqual(result.status, 0, 'Expected validator to fail on workflow-level id-token write');
|
||||
assert.match(result.stderr, /id-token: write must be scoped to a publish-only job/);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('allows job-scoped id-token for publish-only jobs', () => {
|
||||
const result = runValidator({
|
||||
'safe-publish-oidc.yml': `name: Safe\non:\n push:\npermissions:\n contents: read\njobs:\n publish:\n runs-on: ubuntu-latest\n permissions:\n contents: write\n id-token: write\n steps:\n - run: npm publish package.tgz --access public --provenance\n`,
|
||||
});
|
||||
assert.strictEqual(result.status, 0, result.stderr || result.stdout);
|
||||
})) passed++; else failed++;
|
||||
|
||||
if (test('rejects npm audit without registry signature verification', () => {
|
||||
const result = runValidator({
|
||||
'unsafe-audit.yml': `name: Unsafe\non:\n push:\njobs:\n audit:\n runs-on: ubuntu-latest\n steps:\n - run: npm audit --audit-level=high\n`,
|
||||
|
||||
@@ -32,9 +32,11 @@ for (const workflow of [
|
||||
'.github/workflows/reusable-release.yml',
|
||||
]) {
|
||||
const content = load(workflow);
|
||||
const workflowHeader = content.slice(0, content.indexOf('\njobs:\n'));
|
||||
|
||||
test(`${workflow} grants id-token for npm provenance`, () => {
|
||||
assert.match(content, /permissions:\s*[\s\S]*id-token:\s*write/m);
|
||||
test(`${workflow} scopes id-token to the publish job for npm provenance`, () => {
|
||||
assert.doesNotMatch(workflowHeader, /id-token:\s*write/);
|
||||
assert.match(content, /\n\s+permissions:\n\s+contents:\s*write\n\s+id-token:\s*write/m);
|
||||
});
|
||||
|
||||
test(`${workflow} configures the npm registry`, () => {
|
||||
@@ -51,7 +53,7 @@ for (const workflow of [
|
||||
});
|
||||
|
||||
test(`${workflow} publishes new tag versions to npm`, () => {
|
||||
assert.match(content, /npm publish --access public --provenance/);
|
||||
assert.match(content, /npm publish "\$\{\{ needs\.verify\.outputs\.package_file \}\}" --access public --provenance/);
|
||||
assert.match(content, /NODE_AUTH_TOKEN:\s*\$\{\{\s*secrets\.NPM_TOKEN\s*\}\}/);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user