chore(security): harden GitHub workflows

This commit is contained in:
Sinabina
2026-07-20 16:23:41 -07:00
parent 2e1e52eae7
commit 09492d34b4
8 changed files with 128 additions and 0 deletions
+4
View File
@@ -4,6 +4,10 @@ on: [push, pull_request]
permissions: permissions:
contents: read contents: read
concurrency:
group: actionlint-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
actionlint: actionlint:
runs-on: ubicloud-standard-8 runs-on: ubicloud-standard-8
+4
View File
@@ -16,6 +16,10 @@ on:
# Manual trigger # Manual trigger
workflow_dispatch: workflow_dispatch:
concurrency:
group: ci-image
cancel-in-progress: false
jobs: jobs:
build: build:
runs-on: ubicloud-standard-8 runs-on: ubicloud-standard-8
+21
View File
@@ -0,0 +1,21 @@
name: Dependency Review
on:
pull_request:
branches: [main]
permissions:
contents: read
concurrency:
group: dependency-review-${{ github.event.pull_request.number }}
cancel-in-progress: true
jobs:
dependency-review:
runs-on: ubuntu-24.04
steps:
- name: Review dependency changes
uses: actions/dependency-review-action@2031cfc080254a8a887f58cffee85186f0e49e48 # v4.9.0
with:
fail-on-severity: high
+26
View File
@@ -0,0 +1,26 @@
name: OSV Scanner
on:
schedule:
- cron: '23 7 * * 1'
workflow_dispatch:
permissions:
contents: read
concurrency:
group: osv-scanner
cancel-in-progress: true
jobs:
scan:
permissions:
actions: read
contents: read
security-events: write
uses: google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@3adb4b14a2b0623876d18d863a498b785fb3752d # v2.3.8
with:
scan-args: |-
--include-git-root
--recursive
./
+4
View File
@@ -10,6 +10,10 @@ on:
permissions: permissions:
contents: read contents: read
concurrency:
group: quality-gate-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
quality: quality:
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
+46
View File
@@ -0,0 +1,46 @@
name: OpenSSF Scorecard
on:
branch_protection_rule:
schedule:
- cron: '41 7 * * 1'
push:
branches: [main]
permissions:
contents: read
concurrency:
group: scorecard-${{ github.ref }}
cancel-in-progress: true
jobs:
analysis:
runs-on: ubuntu-24.04
permissions:
contents: read
security-events: write
steps:
- name: Checkout repository
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- name: Run Scorecard analysis
uses: ossf/scorecard-action@4eaacf0543bb3f2c246792bd56e8cdeffafb205a # v2.4.3
with:
results_file: results.sarif
results_format: sarif
publish_results: false
- name: Upload Scorecard results to code scanning
uses: github/codeql-action/upload-sarif@85b88275909735f5bc23196090e03d2eb148b3de # v3.32.4
with:
sarif_file: results.sarif
- name: Upload Scorecard results artifact
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: scorecard-results
path: results.sarif
retention-days: 5
+4
View File
@@ -4,6 +4,10 @@ on: [push, pull_request]
permissions: permissions:
contents: read contents: read
concurrency:
group: skill-docs-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
check-freshness: check-freshness:
runs-on: ubicloud-standard-8 runs-on: ubicloud-standard-8
+19
View File
@@ -12,6 +12,7 @@ describe("release and CI hardening", () => {
for (const name of fs.readdirSync(workflowRoot).filter((entry) => entry.endsWith(".yml"))) { for (const name of fs.readdirSync(workflowRoot).filter((entry) => entry.endsWith(".yml"))) {
const source = fs.readFileSync(path.join(workflowRoot, name), "utf8"); const source = fs.readFileSync(path.join(workflowRoot, name), "utf8");
expect(source, `${name} must declare top-level permissions`).toMatch(/^permissions:\s*$/m); expect(source, `${name} must declare top-level permissions`).toMatch(/^permissions:\s*$/m);
expect(source, `${name} must declare workflow concurrency`).toMatch(/^concurrency:\s*$/m);
for (const match of source.matchAll(/\buses:\s*[^\s@]+@([^\s#]+)/g)) { for (const match of source.matchAll(/\buses:\s*[^\s@]+@([^\s#]+)/g)) {
expect(match[1], `${name} contains a mutable action ref`).toMatch(/^[a-f0-9]{40}$/); expect(match[1], `${name} contains a mutable action ref`).toMatch(/^[a-f0-9]{40}$/);
} }
@@ -24,6 +25,24 @@ describe("release and CI hardening", () => {
expect(source.match(new RegExp(guard.replaceAll(".", "\\."), "g"))?.length).toBeGreaterThanOrEqual(3); expect(source.match(new RegExp(guard.replaceAll(".", "\\."), "g"))?.length).toBeGreaterThanOrEqual(3);
}); });
test("public dependency and repository security workflows stay enabled and least-privileged", () => {
const dependencyReview = read(".github/workflows/dependency-review.yml");
expect(dependencyReview).toContain("actions/dependency-review-action@");
expect(dependencyReview).toContain("fail-on-severity: high");
expect(dependencyReview).not.toContain("pull_request_target:");
const osv = read(".github/workflows/osv-scanner.yml");
expect(osv).toContain("google/osv-scanner-action/.github/workflows/osv-scanner-reusable.yml@");
expect(osv).toContain("security-events: write");
expect(osv).toContain("schedule:");
const scorecard = read(".github/workflows/scorecard.yml");
expect(scorecard).toContain("ossf/scorecard-action@");
expect(scorecard).toContain("github/codeql-action/upload-sarif@");
expect(scorecard).toContain("publish_results: false");
expect(scorecard).not.toContain("id-token: write");
});
test("npm package is an explicit small runtime-control surface", () => { test("npm package is an explicit small runtime-control surface", () => {
const pkg = JSON.parse(read("package.json")); const pkg = JSON.parse(read("package.json"));
expect(pkg.version).toBe(read("VERSION").trim()); expect(pkg.version).toBe(read("VERSION").trim());