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:
易良 2026-08-14 01:22:53 +00:00 committed by GitHub
parent 60c338f144
commit fb6637f0d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 261 additions and 2 deletions

4
.github/CODEOWNERS vendored
View file

@ -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

View file

@ -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
View 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

View file

@ -1,5 +1,8 @@
name: 'SDK Python'
permissions:
contents: 'read'
on:
pull_request:
branches:

96
.github/workflows/security-checks.yml vendored Normal file
View 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
View file

@ -17,8 +17,6 @@
# Dependency directory
node_modules
bower_components
package-lock.json
!packages/desktop-shell/package-lock.json
# Editors
.idea

View 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');
});
});