From 981554b90c7c5c797f1fd53d056668ca0a596b2d Mon Sep 17 00:00:00 2001 From: Aleksandar Grbic Date: Sat, 22 Aug 2026 22:30:36 +0200 Subject: [PATCH] fix: close OpenSSF and CodeQL code-scanning alerts Move security-events write to the SARIF-upload jobs, replace existsSync plus writeFileSync with exclusive wx creates, add CODEOWNERS, and tighten desired branch-protection (reviews, code owners, required checks, enforce admins) so Scorecard Branch-Protection / Token-Permissions / Code-Review can pass. --- .github/CODEOWNERS | 1 + .github/desired-repo-settings.json | 7 +++-- .github/workflows/security-deps.yml | 4 ++- .github/workflows/security-sast.yml | 6 ++-- .github/workflows/security-secrets.yml | 7 +++-- scripts/_lib.test.ts | 38 ++++++++++++++++++++++++++ scripts/_lib.ts | 21 ++++++++++---- scripts/audit-repo-settings.sh | 30 ++++++++++++++++++-- vitest.config.ts | 2 +- 9 files changed, 100 insertions(+), 16 deletions(-) create mode 100644 .github/CODEOWNERS create mode 100644 scripts/_lib.test.ts diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..bfec0b6 --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1 @@ +* @agjs diff --git a/.github/desired-repo-settings.json b/.github/desired-repo-settings.json index 19d716a..120edc3 100644 --- a/.github/desired-repo-settings.json +++ b/.github/desired-repo-settings.json @@ -28,10 +28,13 @@ "semgrep SAST", "analyze (javascript-typescript)" ], - "required_approving_review_count": 0, + "required_approving_review_count": 1, "dismiss_stale_reviews": true, + "require_code_owner_reviews": true, + "require_last_push_approval": true, + "enforce_admins": true, "required_linear_history": true, - "required_signatures": true, + "required_signatures": false, "allow_force_pushes": false, "allow_deletions": false } diff --git a/.github/workflows/security-deps.yml b/.github/workflows/security-deps.yml index 1db483c..e8377b9 100644 --- a/.github/workflows/security-deps.yml +++ b/.github/workflows/security-deps.yml @@ -14,13 +14,15 @@ concurrency: permissions: contents: read - security-events: write jobs: scan: name: dep vuln scan (osv + audit) runs-on: ubuntu-24.04 timeout-minutes: 10 + permissions: + contents: read + security-events: write env: OSV_SCANNER_VERSION: '2.3.8' diff --git a/.github/workflows/security-sast.yml b/.github/workflows/security-sast.yml index 8dd490b..6f4d263 100644 --- a/.github/workflows/security-sast.yml +++ b/.github/workflows/security-sast.yml @@ -14,14 +14,16 @@ concurrency: permissions: contents: read - security-events: write - pull-requests: read jobs: semgrep: name: semgrep SAST runs-on: ubuntu-24.04 timeout-minutes: 15 + permissions: + contents: read + security-events: write + pull-requests: read container: image: semgrep/semgrep:1.142.0@sha256:03402a5040a88a570dec58375ef1a19fa777dd61575afdc7d5527ddf308dd765 diff --git a/.github/workflows/security-secrets.yml b/.github/workflows/security-secrets.yml index 0aba584..1ae17e2 100644 --- a/.github/workflows/security-secrets.yml +++ b/.github/workflows/security-secrets.yml @@ -14,14 +14,16 @@ concurrency: permissions: contents: read - security-events: write - pull-requests: read jobs: gitleaks: name: gitleaks secret scan runs-on: ubuntu-24.04 timeout-minutes: 10 + permissions: + contents: read + security-events: write + pull-requests: read env: GITLEAKS_VERSION: '8.30.1' @@ -31,6 +33,7 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 + persist-credentials: false - name: Install gitleaks CLI run: | diff --git a/scripts/_lib.test.ts b/scripts/_lib.test.ts new file mode 100644 index 0000000..4ff6f2f --- /dev/null +++ b/scripts/_lib.test.ts @@ -0,0 +1,38 @@ +import { mkdtempSync, readFileSync, rmSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; + +import { afterEach, describe, expect, it } from 'vitest'; + +import { writeFileSafe } from './_lib.js'; + +describe('writeFileSafe', () => { + const dirs: string[] = []; + + afterEach(() => { + for (const dir of dirs.splice(0)) { + rmSync(dir, { recursive: true, force: true }); + } + }); + + it('creates a missing file and refuses to clobber it without overwrite', () => { + const dir = mkdtempSync(join(tmpdir(), 'phaser-write-')); + dirs.push(dir); + const path = join(dir, 'nested', 'out.ts'); + + expect(writeFileSafe(path, 'one\n')).toBe(true); + expect(readFileSync(path, 'utf8')).toBe('one\n'); + expect(writeFileSafe(path, 'two\n')).toBe(false); + expect(readFileSync(path, 'utf8')).toBe('one\n'); + }); + + it('overwrites when overwrite is true', () => { + const dir = mkdtempSync(join(tmpdir(), 'phaser-write-')); + dirs.push(dir); + const path = join(dir, 'out.ts'); + + expect(writeFileSafe(path, 'one\n')).toBe(true); + expect(writeFileSafe(path, 'two\n', true)).toBe(true); + expect(readFileSync(path, 'utf8')).toBe('two\n'); + }); +}); diff --git a/scripts/_lib.ts b/scripts/_lib.ts index b57adee..41e98e7 100644 --- a/scripts/_lib.ts +++ b/scripts/_lib.ts @@ -1,4 +1,4 @@ -import { mkdirSync, writeFileSync, existsSync } from 'node:fs'; +import { mkdirSync, writeFileSync } from 'node:fs'; import { dirname } from 'node:path'; export const toPascalCase = (s: string): string => @@ -17,13 +17,24 @@ export const toKebabCase = (s: string): string => .replace(/[\s_]+/g, '-') .toLowerCase(); -export const writeFileSafe = (path: string, contents: string, overwrite = false): boolean => { - if (existsSync(path) && !overwrite) { - console.warn(` skip ${path} (exists — use --force to overwrite)`); +const isAlreadyExistsError = (err: unknown): boolean => { + if (typeof err !== 'object' || err === null || !('code' in err)) { return false; } + return typeof err.code === 'string' && err.code === 'EEXIST'; +}; + +export const writeFileSafe = (path: string, contents: string, overwrite = false): boolean => { mkdirSync(dirname(path), { recursive: true }); - writeFileSync(path, contents, 'utf8'); + try { + writeFileSync(path, contents, { encoding: 'utf8', flag: overwrite ? 'w' : 'wx' }); + } catch (err: unknown) { + if (!overwrite && isAlreadyExistsError(err)) { + console.warn(` skip ${path} (exists — use --force to overwrite)`); + return false; + } + throw err; + } console.log(` write ${path}`); return true; }; diff --git a/scripts/audit-repo-settings.sh b/scripts/audit-repo-settings.sh index 752bff8..18884d9 100755 --- a/scripts/audit-repo-settings.sh +++ b/scripts/audit-repo-settings.sh @@ -101,6 +101,21 @@ have_reviews=$(echo "$PROT_CURRENT" | jq -r '.required_pull_request_reviews.requ [[ "$have_reviews" != "$want_reviews" ]] && \ note_protection "required_approving_review_count = $have_reviews (want $want_reviews)" +want_codeowners=$(echo "$PROT_DESIRED" | jq -r '.require_code_owner_reviews // false') +have_codeowners=$(echo "$PROT_CURRENT" | jq -r '.required_pull_request_reviews.require_code_owner_reviews // false') +[[ "$have_codeowners" != "$want_codeowners" ]] && \ + note_protection "require_code_owner_reviews = $have_codeowners (want $want_codeowners)" + +want_last_push=$(echo "$PROT_DESIRED" | jq -r '.require_last_push_approval // false') +have_last_push=$(echo "$PROT_CURRENT" | jq -r '.required_pull_request_reviews.require_last_push_approval // false') +[[ "$have_last_push" != "$want_last_push" ]] && \ + note_protection "require_last_push_approval = $have_last_push (want $want_last_push)" + +want_admins=$(echo "$PROT_DESIRED" | jq -r '.enforce_admins // false') +have_admins=$(echo "$PROT_CURRENT" | jq -r '.enforce_admins.enabled // false') +[[ "$have_admins" != "$want_admins" ]] && \ + note_protection "enforce_admins = $have_admins (want $want_admins)" + # required_linear_history want_lin=$(echo "$PROT_DESIRED" | jq -r '.required_linear_history') have_lin=$(echo "$PROT_CURRENT" | jq -r '.required_linear_history.enabled // false') @@ -120,8 +135,16 @@ if [[ "$protection_drift" -eq 1 ]]; then # Translate desired JSON → PUT body. Reviews count 0 → null (no required reviews). rev_count=$(echo "$PROT_DESIRED" | jq -r '.required_approving_review_count') if [[ "$rev_count" -gt 0 ]]; then - prr_block=$(jq -n --argjson n "$rev_count" \ - '{required_approving_review_count: $n, dismiss_stale_reviews: true, require_code_owner_reviews: false}') + prr_block=$(jq -n \ + --argjson n "$rev_count" \ + --argjson owners "$want_codeowners" \ + --argjson last "$want_last_push" \ + '{ + required_approving_review_count: $n, + dismiss_stale_reviews: true, + require_code_owner_reviews: $owners, + require_last_push_approval: $last + }') else prr_block='null' fi @@ -137,11 +160,12 @@ if [[ "$protection_drift" -eq 1 ]]; then --argjson rsc "$rsc_block" \ --argjson prr "$prr_block" \ --argjson lin "$want_lin" \ + --argjson admins "$want_admins" \ --argjson fp "$(echo "$PROT_DESIRED" | jq '.allow_force_pushes')" \ --argjson del "$(echo "$PROT_DESIRED" | jq '.allow_deletions')" \ '{ required_status_checks: $rsc, - enforce_admins: false, + enforce_admins: $admins, required_pull_request_reviews: $prr, restrictions: null, required_linear_history: $lin, diff --git a/vitest.config.ts b/vitest.config.ts index 2f8b903..05ebe96 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -16,7 +16,7 @@ export default defineConfig({ test: { globals: true, environment: 'jsdom', - include: ['src/**/*.test.ts', 'tests/integration/**/*.test.ts'], + include: ['src/**/*.test.ts', 'tests/integration/**/*.test.ts', 'scripts/**/*.test.ts'], exclude: ['node_modules/**', 'dist/**', 'tests/smoke/**'], coverage: { provider: 'v8',