mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-08-20 22:25:30 +00:00
chore(ci): Add security hygiene: CODEOWNERS for release workflows, least-privilege permissions, security checks and Scorecard (#9008)
* chore(ci): add security hygiene: CODEOWNERS for release workflows, least-privilege permissions, security checks and scorecard workflows * chore(ci): pin TruffleHog scanner version and drop invalid path input * fix(ci): close security workflow review gaps * fix(ci): fail package audit on install errors * test(ci): pin security workflow guardrails * fix(ci): pin security workflow test assertions for SHA refs, status edges, and push trigger * test(ci): pin security workflow edge guards * test(ci): pin security workflow contracts * test(ci): pin secret-scan push guard * fix(ci): quote secret-scan condition * fix(ci): audit workspace package locks directly * fix(ci): scope security checks concurrency * docs(ci): explain mobile audit skip * test(ci): link trufflehog version pin
This commit is contained in:
parent
60c338f144
commit
fb6637f0d3
7 changed files with 261 additions and 2 deletions
4
.github/CODEOWNERS
vendored
4
.github/CODEOWNERS
vendored
|
|
@ -5,6 +5,10 @@
|
|||
# --- CODEOWNERS file itself ---
|
||||
/.github/CODEOWNERS @pomelo-nwu @wenshao
|
||||
|
||||
# --- Primary npm release workflows require core maintainer approval ---
|
||||
/.github/workflows/release.yml @pomelo-nwu @wenshao
|
||||
/.github/workflows/finalize-release.yml @pomelo-nwu @wenshao
|
||||
|
||||
# --- Core package ---
|
||||
/packages/core/ @wenshao @tanzhenxin @yiliang114 @LaZzyMan @doudouOUC
|
||||
|
||||
|
|
|
|||
3
.github/workflows/e2e.yml
vendored
3
.github/workflows/e2e.yml
vendored
|
|
@ -1,5 +1,8 @@
|
|||
name: 'E2E Tests'
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
|
||||
on:
|
||||
# E2E is slow and currently flaky, so it is NOT in the merge queue (gating the
|
||||
# serial queue on it would stall every merge). It runs post-merge on `main`,
|
||||
|
|
|
|||
43
.github/workflows/scorecard-monthly.yml
vendored
Normal file
43
.github/workflows/scorecard-monthly.yml
vendored
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# .github/workflows/scorecard-monthly.yml
|
||||
|
||||
name: 'Scorecard Monthly'
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# 02:00 UTC on the first day of each month.
|
||||
- cron: '0 2 1 * *'
|
||||
workflow_dispatch: {}
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
scorecard:
|
||||
name: 'OpenSSF Scorecard'
|
||||
runs-on: 'ubuntu-latest'
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Run Scorecard'
|
||||
uses: 'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc' # v2.4.4
|
||||
with:
|
||||
results_file: 'results.json'
|
||||
results_format: 'json'
|
||||
publish_results: false
|
||||
env:
|
||||
GITHUB_AUTH_TOKEN: '${{ secrets.GITHUB_TOKEN }}'
|
||||
|
||||
- name: 'Upload results'
|
||||
uses: 'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02' # v4.6.2
|
||||
with:
|
||||
name: 'scorecard-${{ github.run_id }}'
|
||||
path: 'results.json'
|
||||
retention-days: 90
|
||||
3
.github/workflows/sdk-python.yml
vendored
3
.github/workflows/sdk-python.yml
vendored
|
|
@ -1,5 +1,8 @@
|
|||
name: 'SDK Python'
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
|
|
|
|||
96
.github/workflows/security-checks.yml
vendored
Normal file
96
.github/workflows/security-checks.yml
vendored
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# .github/workflows/security-checks.yml
|
||||
|
||||
name: 'Security Checks'
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'release/**'
|
||||
push:
|
||||
branches:
|
||||
- 'main'
|
||||
- 'release/**'
|
||||
|
||||
concurrency:
|
||||
group: '${{ github.workflow }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.ref }}'
|
||||
cancel-in-progress: "${{ github.event_name == 'pull_request' }}"
|
||||
|
||||
permissions:
|
||||
contents: 'read'
|
||||
|
||||
defaults:
|
||||
run:
|
||||
shell: 'bash'
|
||||
|
||||
jobs:
|
||||
dependency-cve:
|
||||
name: 'Dependency CVE audit'
|
||||
runs-on: 'ubuntu-latest'
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
|
||||
- name: 'Set up Node'
|
||||
uses: 'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e' # v6.4.0
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
cache: 'npm'
|
||||
|
||||
# --ignore-scripts: CI must not run dependency install hooks. The audit
|
||||
# only needs the resolved dependency tree, so skipping postinstall
|
||||
# (patch-package) is safe here.
|
||||
- name: 'Install dependencies'
|
||||
run: 'npm ci --ignore-scripts --no-audit --progress=false'
|
||||
|
||||
# Reporting-only for now: the current baseline already contains high
|
||||
# severity findings, so failing the check would block every PR. Remove
|
||||
# continue-on-error once the baseline is clean to make this a hard gate,
|
||||
# and update scripts/tests/security-workflows.test.js with that policy.
|
||||
- name: 'Audit production dependencies'
|
||||
continue-on-error: true
|
||||
run: |
|
||||
status=0
|
||||
npm audit --omit=dev --audit-level=high || status=$?
|
||||
for lockfile in packages/*/package-lock.json; do
|
||||
[ -f "$lockfile" ] || continue
|
||||
# Covered by the root workspace audit; this vendored lockfile is not installed directly.
|
||||
[ "$lockfile" != "packages/mobile-mcp/package-lock.json" ] || continue
|
||||
package_dir="${lockfile%/package-lock.json}"
|
||||
(
|
||||
cd "$package_dir"
|
||||
npm ci --ignore-scripts --no-audit --progress=false --workspaces=false &&
|
||||
npm audit --omit=dev --audit-level=high --workspaces=false
|
||||
) || status=$?
|
||||
done
|
||||
exit "$status"
|
||||
|
||||
secret-scan:
|
||||
name: 'Secret scan (TruffleHog)'
|
||||
runs-on: 'ubuntu-latest'
|
||||
timeout-minutes: 15
|
||||
steps:
|
||||
- name: 'Checkout'
|
||||
uses: 'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10' # v6.0.3
|
||||
with:
|
||||
persist-credentials: false
|
||||
fetch-depth: 0
|
||||
|
||||
# Incremental gate: the action scans commits introduced by the PR or
|
||||
# push and only fails on secrets TruffleHog could actively verify,
|
||||
# keeping false positives out. Branch-creating pushes have no base
|
||||
# commit to diff from, so they are skipped explicitly. Reporting-only
|
||||
# for the first runs; remove continue-on-error once the history has been
|
||||
# triaged, and update scripts/tests/security-workflows.test.js with
|
||||
# that policy.
|
||||
# `version` pins the scanner image; without it the action runs `latest`.
|
||||
- name: 'Scan for verified secrets'
|
||||
if: "github.event_name == 'pull_request' || github.event.before != '0000000000000000000000000000000000000000'"
|
||||
uses: 'trufflesecurity/trufflehog@6f3c981e7b77f235fd2702dd74af25fc4b72bf11' # v3.96.0
|
||||
continue-on-error: true
|
||||
with:
|
||||
version: '3.96.0'
|
||||
extra_args: '--only-verified'
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
|
|
@ -17,8 +17,6 @@
|
|||
# Dependency directory
|
||||
node_modules
|
||||
bower_components
|
||||
package-lock.json
|
||||
!packages/desktop-shell/package-lock.json
|
||||
|
||||
# Editors
|
||||
.idea
|
||||
|
|
|
|||
112
scripts/tests/security-workflows.test.js
Normal file
112
scripts/tests/security-workflows.test.js
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
/**
|
||||
* @license
|
||||
* Copyright 2026 Qwen Team
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { readFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { getWorkflowJob, getWorkflowStep } from './workflow-helpers.js';
|
||||
|
||||
const repoRoot = path.resolve(
|
||||
path.dirname(fileURLToPath(import.meta.url)),
|
||||
'../..',
|
||||
);
|
||||
|
||||
const readWorkflow = (name) =>
|
||||
readFileSync(path.join(repoRoot, `.github/workflows/${name}`), 'utf8');
|
||||
|
||||
describe('security workflows', () => {
|
||||
it('keeps Scorecard monthly and reporting-only', () => {
|
||||
const workflow = readWorkflow('scorecard-monthly.yml');
|
||||
|
||||
expect(workflow).toContain("- cron: '0 2 1 * *'");
|
||||
expect(workflow).toContain('workflow_dispatch: {}');
|
||||
expect(workflow).not.toContain('pull_request');
|
||||
expect(workflow).toContain('publish_results: false');
|
||||
expect(workflow).toContain('retention-days: 90');
|
||||
expect(workflow).toContain(
|
||||
'ossf/scorecard-action@2d1146689b8cda280b9bc96326124645441f03bc',
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10',
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
'actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02',
|
||||
);
|
||||
expect(workflow).toContain('persist-credentials: false');
|
||||
});
|
||||
|
||||
it('keeps Security Checks reporting-only and audits package locks', () => {
|
||||
const workflow = readWorkflow('security-checks.yml');
|
||||
const dependencyJob = getWorkflowJob(workflow, 'dependency-cve');
|
||||
const dependencyCheckoutStep = getWorkflowStep(dependencyJob, 'Checkout');
|
||||
const installStep = getWorkflowStep(dependencyJob, 'Install dependencies');
|
||||
const auditStep = getWorkflowStep(
|
||||
dependencyJob,
|
||||
'Audit production dependencies',
|
||||
);
|
||||
const secretScanJob = getWorkflowJob(workflow, 'secret-scan');
|
||||
const checkoutStep = getWorkflowStep(secretScanJob, 'Checkout');
|
||||
const trufflehogStep = getWorkflowStep(
|
||||
secretScanJob,
|
||||
'Scan for verified secrets',
|
||||
);
|
||||
|
||||
expect(workflow).toContain('pull_request:');
|
||||
expect(workflow).toContain('push:');
|
||||
expect(workflow).toContain(
|
||||
"group: '${{ github.workflow }}-${{ github.event.pull_request.head.repo.full_name || github.repository }}-${{ github.head_ref || github.ref }}'",
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
'cancel-in-progress: "${{ github.event_name == \'pull_request\' }}"',
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
'actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10',
|
||||
);
|
||||
expect(workflow).toContain(
|
||||
'actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e',
|
||||
);
|
||||
expect(dependencyCheckoutStep).toContain('persist-credentials: false');
|
||||
expect(checkoutStep).toContain('persist-credentials: false');
|
||||
expect(installStep).toContain(
|
||||
"run: 'npm ci --ignore-scripts --no-audit --progress=false'",
|
||||
);
|
||||
expect(auditStep).toContain('continue-on-error: true');
|
||||
expect(auditStep).toContain('status=0');
|
||||
expect(auditStep).toContain('exit "$status"');
|
||||
expect(auditStep).toContain('npm audit --omit=dev --audit-level=high');
|
||||
expect(auditStep).toContain(
|
||||
'npm audit --omit=dev --audit-level=high || status=$?',
|
||||
);
|
||||
expect(auditStep).toContain(') || status=$?');
|
||||
expect(auditStep).toContain('for lockfile in packages/*/package-lock.json');
|
||||
expect(auditStep).toContain('[ -f "$lockfile" ] || continue');
|
||||
expect(auditStep).toContain(
|
||||
'[ "$lockfile" != "packages/mobile-mcp/package-lock.json" ] || continue',
|
||||
);
|
||||
expect(auditStep).toContain('cd "$package_dir"');
|
||||
expect(auditStep).toContain(
|
||||
'npm ci --ignore-scripts --no-audit --progress=false --workspaces=false &&',
|
||||
);
|
||||
expect(auditStep).toContain(
|
||||
'npm audit --omit=dev --audit-level=high --workspaces=false',
|
||||
);
|
||||
expect(trufflehogStep).toContain('continue-on-error: true');
|
||||
const trufflehogPin = trufflehogStep.match(
|
||||
/trufflesecurity\/trufflehog@[0-9a-f]{40}' # v([\d.]+)/,
|
||||
);
|
||||
expect(trufflehogPin).not.toBeNull();
|
||||
expect(trufflehogStep).toContain(`version: '${trufflehogPin?.[1]}'`);
|
||||
expect(trufflehogStep).toContain(
|
||||
"if: \"github.event_name == 'pull_request' || github.event.before != '0000000000000000000000000000000000000000'\"",
|
||||
);
|
||||
expect(trufflehogStep).toContain("extra_args: '--only-verified'");
|
||||
expect(trufflehogStep).toContain(
|
||||
'trufflesecurity/trufflehog@6f3c981e7b77f235fd2702dd74af25fc4b72bf11',
|
||||
);
|
||||
expect(checkoutStep).toContain('fetch-depth: 0');
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue