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*']
|
tags: ['v*']
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: read
|
||||||
id-token: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
verify:
|
||||||
name: Create Release
|
name: Verify Release
|
||||||
runs-on: ubuntu-latest
|
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:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -97,6 +100,42 @@ jobs:
|
|||||||
- For migration tips and compatibility notes, see README and CHANGELOG.
|
- For migration tips and compatibility notes, see README and CHANGELOG.
|
||||||
EOF
|
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
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||||
with:
|
with:
|
||||||
@@ -106,7 +145,7 @@ jobs:
|
|||||||
make_latest: ${{ contains(github.ref_name, '-') && 'false' || 'true' }}
|
make_latest: ${{ contains(github.ref_name, '-') && 'false' || 'true' }}
|
||||||
|
|
||||||
- name: Publish npm package
|
- name: Publish npm package
|
||||||
if: steps.npm_publish_state.outputs.already_published != 'true'
|
if: needs.verify.outputs.already_published != 'true'
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
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
|
default: true
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: read
|
||||||
id-token: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
release:
|
verify:
|
||||||
name: Create Release
|
name: Verify Release
|
||||||
runs-on: ubuntu-latest
|
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:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
@@ -114,6 +117,42 @@ jobs:
|
|||||||
- Claude marketplace/plugin identifier: \`everything-claude-code@everything-claude-code\`
|
- Claude marketplace/plugin identifier: \`everything-claude-code@everything-claude-code\`
|
||||||
EOF
|
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
|
- name: Create GitHub Release
|
||||||
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
|
||||||
with:
|
with:
|
||||||
@@ -124,7 +163,7 @@ jobs:
|
|||||||
make_latest: ${{ contains(inputs.tag, '-') && 'false' || 'true' }}
|
make_latest: ${{ contains(inputs.tag, '-') && 'false' || 'true' }}
|
||||||
|
|
||||||
- name: Publish npm package
|
- name: Publish npm package
|
||||||
if: steps.npm_publish_state.outputs.already_published != 'true'
|
if: needs.verify.outputs.already_published != 'true'
|
||||||
env:
|
env:
|
||||||
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
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 NPM_AUDIT_SIGNATURES_PATTERN = /\bnpm\s+audit\s+signatures\b/;
|
||||||
const ACTIONS_CACHE_PATTERN = /uses:\s*['"]?actions\/cache@/m;
|
const ACTIONS_CACHE_PATTERN = /uses:\s*['"]?actions\/cache@/m;
|
||||||
const ID_TOKEN_WRITE_PATTERN = /^\s*id-token:\s*write\b/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 = [
|
const UNSAFE_INSTALL_PATTERNS = [
|
||||||
{
|
{
|
||||||
pattern: /\bnpm\s+ci\b(?![^\n]*--ignore-scripts)/g,
|
pattern: /\bnpm\s+ci\b(?![^\n]*--ignore-scripts)/g,
|
||||||
@@ -121,6 +122,8 @@ function extractCheckoutSteps(source) {
|
|||||||
function findViolations(filePath, source) {
|
function findViolations(filePath, source) {
|
||||||
const violations = [];
|
const violations = [];
|
||||||
const checkoutSteps = extractCheckoutSteps(source);
|
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) {
|
for (const rule of RULES) {
|
||||||
if (!rule.eventPattern.test(source)) {
|
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 installRule of UNSAFE_INSTALL_PATTERNS) {
|
||||||
for (const match of source.matchAll(installRule.pattern)) {
|
for (const match of source.matchAll(installRule.pattern)) {
|
||||||
violations.push({
|
violations.push({
|
||||||
|
|||||||
@@ -244,12 +244,27 @@ function run() {
|
|||||||
|
|
||||||
if (test('rejects actions/cache in workflows with id-token write', () => {
|
if (test('rejects actions/cache in workflows with id-token write', () => {
|
||||||
const result = runValidator({
|
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.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/);
|
assert.match(result.stderr, /id-token: write must not restore or save shared dependency caches/);
|
||||||
})) passed++; else failed++;
|
})) 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', () => {
|
if (test('rejects npm audit without registry signature verification', () => {
|
||||||
const result = runValidator({
|
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`,
|
'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',
|
'.github/workflows/reusable-release.yml',
|
||||||
]) {
|
]) {
|
||||||
const content = load(workflow);
|
const content = load(workflow);
|
||||||
|
const workflowHeader = content.slice(0, content.indexOf('\njobs:\n'));
|
||||||
|
|
||||||
test(`${workflow} grants id-token for npm provenance`, () => {
|
test(`${workflow} scopes id-token to the publish job for npm provenance`, () => {
|
||||||
assert.match(content, /permissions:\s*[\s\S]*id-token:\s*write/m);
|
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`, () => {
|
test(`${workflow} configures the npm registry`, () => {
|
||||||
@@ -51,7 +53,7 @@ for (const workflow of [
|
|||||||
});
|
});
|
||||||
|
|
||||||
test(`${workflow} publishes new tag versions to npm`, () => {
|
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*\}\}/);
|
assert.match(content, /NODE_AUTH_TOKEN:\s*\$\{\{\s*secrets\.NPM_TOKEN\s*\}\}/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user