From 2ebb55e328602c59e24ae4bf158c0c18878eb39d Mon Sep 17 00:00:00 2001 From: w1ck3ds0d4 <57948187+w1ck3ds0d4@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:35:12 +0200 Subject: [PATCH 1/3] feat(gate): add severity gate (fail-on thresholds) Reads scanner reports, buckets findings by severity, and exits non-zero when a finding meets or exceeds fail-on. Detected secrets always count as critical. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/gate.mjs | 105 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 scripts/gate.mjs diff --git a/scripts/gate.mjs b/scripts/gate.mjs new file mode 100644 index 0000000..46fa4d8 --- /dev/null +++ b/scripts/gate.mjs @@ -0,0 +1,105 @@ +// Severity gate: read the SARIF/JSON reports produced by the scanners, bucket +// findings by severity, and exit non-zero when a finding meets or exceeds the +// configured `fail-on` threshold. Detected secrets are always treated as critical. +// +// Env: +// FAIL_ON none | low | medium | high | critical (default: none) +// REPORTS directory containing gitleaks.sarif, semgrep.sarif, trivy.json +// +// Exit code 0 = pass, 1 = gate tripped. + +import fs from 'node:fs'; +import path from 'node:path'; + +const REPORTS = process.env.REPORTS || '/tmp'; +const failOn = (process.env.FAIL_ON || 'none').toLowerCase(); + +const RANK = { none: 0, low: 1, medium: 2, high: 3, critical: 4 }; +const buckets = { critical: 0, high: 0, medium: 0, low: 0 }; + +function readJson(file) { + try { + return JSON.parse(fs.readFileSync(path.join(REPORTS, file), 'utf8')); + } catch { + return null; + } +} + +// Gitleaks: any secret is critical. +const gitleaks = readJson('gitleaks.sarif'); +let secrets = 0; +if (gitleaks?.runs) { + for (const run of gitleaks.runs) secrets += run.results?.length ?? 0; +} +buckets.critical += secrets; + +// Semgrep SARIF: map level -> severity. +const semgrep = readJson('semgrep.sarif'); +if (semgrep?.runs) { + for (const run of semgrep.runs) { + for (const res of run.results ?? []) { + const level = res.level ?? 'warning'; + if (level === 'error') buckets.high++; + else if (level === 'warning') buckets.medium++; + else buckets.low++; + } + } +} + +// Trivy JSON: vulnerabilities, misconfigurations and licenses carry a Severity. +const trivy = readJson('trivy.json'); +function bump(sev) { + const key = String(sev || '').toLowerCase(); + if (key in buckets) buckets[key]++; +} +if (trivy?.Results) { + for (const result of trivy.Results) { + for (const v of result.Vulnerabilities ?? []) bump(v.Severity); + for (const m of result.Misconfigurations ?? []) bump(m.Severity); + for (const l of result.Licenses ?? []) bump(l.Severity); + } +} + +const maxSeverity = + buckets.critical > 0 ? 4 : buckets.high > 0 ? 3 : buckets.medium > 0 ? 2 : buckets.low > 0 ? 1 : 0; +const threshold = RANK[failOn] ?? 0; + +const line = `Severity counts -> critical: ${buckets.critical}, high: ${buckets.high}, medium: ${buckets.medium}, low: ${buckets.low}`; +console.log(line); + +// Job summary (best effort). +if (process.env.GITHUB_STEP_SUMMARY) { + const tripped = threshold !== 0 && maxSeverity >= threshold; + const summary = [ + '## SecureCheck', + '', + '| Severity | Count |', + '| --- | --- |', + `| Critical | ${buckets.critical} |`, + `| High | ${buckets.high} |`, + `| Medium | ${buckets.medium} |`, + `| Low | ${buckets.low} |`, + '', + `Gate: \`fail-on=${failOn}\` -> ${tripped ? 'FAILED' : 'passed'}`, + '', + ].join('\n'); + try { + fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary); + } catch { + /* ignore */ + } +} + +if (threshold === 0) { + console.log('fail-on=none; reporting only, not gating the build.'); + process.exit(0); +} + +if (maxSeverity >= threshold) { + console.error(`Gate FAILED: found a finding at or above "${failOn}" severity.`); + if (secrets > 0) console.error(` ${secrets} secret(s) detected (always critical).`); + process.exit(1); +} + +console.log(`Gate passed: nothing at or above "${failOn}" severity.`); +process.exit(0); From f1f93e777df3da57fc69a1cd5f508e1fc73f47b9 Mon Sep 17 00:00:00 2001 From: w1ck3ds0d4 <57948187+w1ck3ds0d4@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:35:13 +0200 Subject: [PATCH 2/3] refactor(notify): read reports dir from REPORTS env Falls back to /tmp so existing callers keep working. Co-Authored-By: Claude Opus 4.8 (1M context) --- scripts/notify.mjs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/scripts/notify.mjs b/scripts/notify.mjs index c314688..ce72013 100644 --- a/scripts/notify.mjs +++ b/scripts/notify.mjs @@ -2,6 +2,7 @@ // Posts on every run so each repo's scanner activity is visible in the channel. import fs from 'node:fs'; +import path from 'node:path'; const env = process.env; @@ -187,7 +188,8 @@ function topDurations(d) { function readSevereClaudeFindings() { if (env.CLAUDE_ENABLED !== 'true') return []; try { - const raw = fs.readFileSync('/tmp/claude.json', 'utf8'); + const reportsDir = env.REPORTS || '/tmp'; + const raw = fs.readFileSync(path.join(reportsDir, 'claude.json'), 'utf8'); const arr = JSON.parse(raw); if (!Array.isArray(arr)) return []; return arr.filter(f => f && (f.severity === 'critical' || f.severity === 'high')); From 521056c04e7d4d9e3a8d6a278d2149e5a3d6c65e Mon Sep 17 00:00:00 2001 From: w1ck3ds0d4 <57948187+w1ck3ds0d4@users.noreply.github.com> Date: Thu, 4 Jun 2026 13:35:14 +0200 Subject: [PATCH 3/3] feat(action): add composite action with SARIF upload and caching Composite entry point so SecureCheck is usable as a single uses: step and listable on the Marketplace. Emits SARIF from Gitleaks, Semgrep and Trivy to code scanning, caches pip/npm/Trivy DB, and runs the severity gate. Co-Authored-By: Claude Opus 4.8 (1M context) --- action.yml | 398 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 398 insertions(+) create mode 100644 action.yml diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..196bb0c --- /dev/null +++ b/action.yml @@ -0,0 +1,398 @@ +name: 'SecureCheck' +description: 'Multi-scanner security pipeline (secrets, SAST, dependency CVEs, IaC and license checks) with SARIF upload to code scanning, severity gating, and optional AI PR review.' +author: 'w1ck3ds0d4' + +branding: + icon: 'shield' + color: 'green' + +inputs: + node-version: + description: 'Node.js version for JS/TS tooling.' + default: '20' + python-version: + description: 'Python version for Semgrep / ruff / lizard.' + default: '3.11' + dotnet-version: + description: 'Optional .NET SDK version (only used when a .NET project is detected).' + default: '10.0.x' + fail-on: + description: 'Minimum finding severity that fails the build: none | low | medium | high | critical. Detected secrets are always treated as critical.' + default: 'none' + upload-sarif: + description: 'Upload SARIF results to GitHub code scanning. Requires the calling job to grant `security-events: write`.' + default: 'true' + run-claude: + description: 'Optional Claude PR review: auto | true | false. `auto` runs only on pull_request events when an API key is supplied.' + default: 'auto' + gitleaks-version: + description: 'Pinned Gitleaks release to install.' + default: '8.24.3' + anthropic-api-key: + description: 'Anthropic API key. When set (and on a pull request), enables the Claude security review.' + default: '' + discord-webhook: + description: 'Optional Discord webhook URL. When set, posts a single summary embed.' + default: '' + +runs: + using: 'composite' + steps: + - name: SecureCheck init + shell: bash + run: | + set -euo pipefail + echo "REPORTS=${RUNNER_TEMP}/securecheck" >> "$GITHUB_ENV" + mkdir -p "${RUNNER_TEMP}/securecheck" + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ inputs.node-version }} + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: securecheck-pip-${{ runner.os }} + + - name: Cache npm + uses: actions/cache@v4 + with: + path: ~/.npm + key: securecheck-npm-${{ runner.os }} + + - name: Cache Trivy DB + uses: actions/cache@v4 + with: + path: ~/.cache/trivy + key: securecheck-trivy-db-${{ runner.os }} + + - name: Detect stack + id: detect + shell: bash + run: | + set -euo pipefail + has_js=false; has_py=false; has_rust=false; has_dotnet=false + [ -f package.json ] && has_js=true + { [ -f requirements.txt ] || [ -f pyproject.toml ]; } && has_py=true || true + [ -f Cargo.toml ] || find . -maxdepth 4 -name Cargo.toml -not -path './node_modules/*' -not -path './target/*' -print -quit | grep -q . && has_rust=true || true + find . -maxdepth 4 \( -name '*.sln' -o -name '*.slnx' -o -name '*.csproj' \) -print -quit | grep -q . && has_dotnet=true || true + eslint_cfg=false + if [ "${has_js}" = "true" ]; then + for f in .eslintrc .eslintrc.json .eslintrc.js .eslintrc.cjs .eslintrc.yml .eslintrc.yaml eslint.config.js eslint.config.cjs eslint.config.mjs; do + [ -f "$f" ] && eslint_cfg=true && break + done + fi + { + echo "has_js=${has_js}" + echo "has_py=${has_py}" + echo "has_rust=${has_rust}" + echo "has_dotnet=${has_dotnet}" + echo "eslint_cfg=${eslint_cfg}" + } >> "$GITHUB_OUTPUT" + + - name: Install Gitleaks + shell: bash + env: + GITLEAKS_VERSION: ${{ inputs.gitleaks-version }} + run: | + set -euo pipefail + curl -sSfL "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" | tar -xz -C "${RUNNER_TEMP}" + sudo mv "${RUNNER_TEMP}/gitleaks" /usr/local/bin/ + + - name: Gitleaks (secrets) + id: gitleaks + shell: bash + run: | + T0=$(date +%s) + gitleaks detect --source . --redact --no-banner \ + --report-format sarif --report-path "${REPORTS}/gitleaks.sarif" --exit-code 0 || true + COUNT=$(jq '[.runs[].results[]?] | length' "${REPORTS}/gitleaks.sarif" 2>/dev/null || echo 0) + echo "count=${COUNT}" >> "$GITHUB_OUTPUT" + echo "duration=$(( $(date +%s) - T0 ))" >> "$GITHUB_OUTPUT" + echo "Gitleaks findings: ${COUNT}" + + - name: Semgrep (SAST) + id: semgrep + shell: bash + run: | + T0=$(date +%s) + python -m pip install --quiet semgrep + semgrep --config=auto --sarif --output "${REPORTS}/semgrep.sarif" --quiet . || true + COUNT=$(jq '[.runs[].results[]?] | length' "${REPORTS}/semgrep.sarif" 2>/dev/null || echo 0) + echo "count=${COUNT}" >> "$GITHUB_OUTPUT" + echo "duration=$(( $(date +%s) - T0 ))" >> "$GITHUB_OUTPUT" + echo "Semgrep findings: ${COUNT}" + + # Two Trivy passes: JSON drives the detailed counts and the severity gate; + # SARIF feeds code scanning. The DB download is shared via the Trivy cache. + - name: Trivy (JSON for counts + gate) + uses: aquasecurity/trivy-action@0.35.0 + with: + scan-type: fs + format: json + output: ${{ env.REPORTS }}/trivy.json + exit-code: '0' + scanners: vuln,misconfig,license + ignore-unfixed: false + + - name: Trivy (SARIF for code scanning) + uses: aquasecurity/trivy-action@0.35.0 + with: + scan-type: fs + format: sarif + output: ${{ env.REPORTS }}/trivy.sarif + exit-code: '0' + scanners: vuln,misconfig,license + ignore-unfixed: false + + - name: Count Trivy findings + id: trivy + shell: bash + run: | + T0=$(date +%s) + VULN=$(jq '[.Results[]?.Vulnerabilities // [] | length] | add // 0' "${REPORTS}/trivy.json" 2>/dev/null || echo 0) + MISC=$(jq '[.Results[]?.Misconfigurations // [] | length] | add // 0' "${REPORTS}/trivy.json" 2>/dev/null || echo 0) + LIC=$(jq '[.Results[]?.Licenses // [] | length] | add // 0' "${REPORTS}/trivy.json" 2>/dev/null || echo 0) + COUNT=$(( VULN + MISC + LIC )) + { + echo "count=${COUNT}" + echo "vuln=${VULN}" + echo "misconfig=${MISC}" + echo "license=${LIC}" + echo "duration=$(( $(date +%s) - T0 ))" + } >> "$GITHUB_OUTPUT" + echo "Trivy findings: total=${COUNT} vuln=${VULN} misconfig=${MISC} license=${LIC}" + + - name: ESLint (JS/TS) + id: eslint + if: steps.detect.outputs.has_js == 'true' && steps.detect.outputs.eslint_cfg == 'true' + continue-on-error: true + shell: bash + run: | + T0=$(date +%s) + set +e + if [ -f pnpm-lock.yaml ]; then + corepack enable >/dev/null 2>&1 || true + npm install -g pnpm@latest >/dev/null 2>&1 + pnpm install --frozen-lockfile --ignore-scripts >"${REPORTS}/eslint-install.log" 2>&1 + pnpm exec eslint . --format json --output-file "${REPORTS}/eslint.json" + elif [ -f yarn.lock ]; then + corepack enable >/dev/null 2>&1 || true + yarn install --frozen-lockfile --ignore-scripts >"${REPORTS}/eslint-install.log" 2>&1 + yarn eslint . --format json --output-file "${REPORTS}/eslint.json" + elif [ -f package-lock.json ]; then + npm ci --ignore-scripts >"${REPORTS}/eslint-install.log" 2>&1 + npx --no-install eslint . --format json --output-file "${REPORTS}/eslint.json" + else + npm install --no-audit --no-fund --ignore-scripts >"${REPORTS}/eslint-install.log" 2>&1 + npx eslint . --format json --output-file "${REPORTS}/eslint.json" + fi + COUNT=$(jq '[.[].messages | length] | add // 0' "${REPORTS}/eslint.json" 2>/dev/null || echo 0) + echo "count=${COUNT}" >> "$GITHUB_OUTPUT" + echo "duration=$(( $(date +%s) - T0 ))" >> "$GITHUB_OUTPUT" + echo "ESLint findings: ${COUNT}" + + - name: ruff (Python) + id: ruff + if: steps.detect.outputs.has_py == 'true' + continue-on-error: true + shell: bash + run: | + T0=$(date +%s) + python -m pip install --quiet ruff + ruff check --output-format=json --exit-zero . > "${REPORTS}/ruff-check.json" 2>/dev/null || echo '[]' > "${REPORTS}/ruff-check.json" + ruff format --check . > "${REPORTS}/ruff-format.txt" 2>&1 || true + CHECK_COUNT=$(jq 'length' "${REPORTS}/ruff-check.json" 2>/dev/null || echo 0) + FORMAT_COUNT=$(grep -cE '^Would reformat:' "${REPORTS}/ruff-format.txt" || echo 0) + COUNT=$(( CHECK_COUNT + FORMAT_COUNT )) + echo "count=${COUNT}" >> "$GITHUB_OUTPUT" + echo "duration=$(( $(date +%s) - T0 ))" >> "$GITHUB_OUTPUT" + echo "ruff findings: ${COUNT} (lint=${CHECK_COUNT}, format=${FORMAT_COUNT})" + + - name: Set up Rust toolchain + if: steps.detect.outputs.has_rust == 'true' + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt,clippy + + - name: cargo fmt + clippy (Rust) + id: rust + if: steps.detect.outputs.has_rust == 'true' + continue-on-error: true + shell: bash + run: | + T0=$(date +%s) + CARGO_DIR=$(find . -maxdepth 4 -name Cargo.toml -not -path './node_modules/*' -not -path './target/*' | head -n 1 | xargs -r dirname) + [ -z "${CARGO_DIR}" ] && CARGO_DIR="." + cd "${CARGO_DIR}" + set +e + cargo fmt --all --check > "${REPORTS}/cargo-fmt.txt" 2>&1 + FMT_EC=$? + cargo clippy --all-targets --message-format json -- -D warnings > "${REPORTS}/cargo-clippy.json" 2>/dev/null + CLIPPY_COUNT=$(jq -s '[.[] | select(.reason=="compiler-message" and .message.level=="warning") | .message.level] | length' "${REPORTS}/cargo-clippy.json" 2>/dev/null || echo 0) + FMT_COUNT=$([ "$FMT_EC" -ne 0 ] && echo 1 || echo 0) + COUNT=$(( CLIPPY_COUNT + FMT_COUNT )) + echo "count=${COUNT}" >> "$GITHUB_OUTPUT" + echo "duration=$(( $(date +%s) - T0 ))" >> "$GITHUB_OUTPUT" + echo "Rust findings: ${COUNT} (clippy=${CLIPPY_COUNT}, fmt_dirty=${FMT_COUNT})" + + - name: Set up .NET SDK + if: steps.detect.outputs.has_dotnet == 'true' + uses: actions/setup-dotnet@v4 + with: + dotnet-version: ${{ inputs.dotnet-version }} + + - name: dotnet format (.NET) + id: dotnet + if: steps.detect.outputs.has_dotnet == 'true' + continue-on-error: true + shell: bash + run: | + T0=$(date +%s) + set +e + SLN=$(find . -maxdepth 4 \( -name '*.slnx' -o -name '*.sln' \) | head -n 1) + if [ -z "${SLN}" ]; then + SLN=$(find . -maxdepth 4 -name '*.csproj' | head -n 1) + fi + dotnet format "${SLN}" --verify-no-changes --report "${REPORTS}/dotnet-format.json" --severity error > "${REPORTS}/dotnet-format.log" 2>&1 + EC=$? + if [ -f "${REPORTS}/dotnet-format.json" ]; then + COUNT=$(jq '[.[] | .FileChanges // [] | length] | add // 0' "${REPORTS}/dotnet-format.json" 2>/dev/null || echo 0) + else + COUNT=$([ "$EC" -ne 0 ] && echo 1 || echo 0) + fi + echo "count=${COUNT}" >> "$GITHUB_OUTPUT" + echo "duration=$(( $(date +%s) - T0 ))" >> "$GITHUB_OUTPUT" + echo "dotnet format findings: ${COUNT}" + + - name: Complexity (lizard) + id: lizard + continue-on-error: true + shell: bash + run: | + T0=$(date +%s) + python -m pip install --quiet lizard + lizard -C 15 -w . > "${REPORTS}/lizard.txt" 2>&1 || true + COUNT=$(grep -cE '^[^!]' "${REPORTS}/lizard.txt" || echo 0) + echo "count=${COUNT}" >> "$GITHUB_OUTPUT" + echo "duration=$(( $(date +%s) - T0 ))" >> "$GITHUB_OUTPUT" + echo "lizard hotspots (CCN>=15): ${COUNT}" + + - name: Duplication (jscpd) + id: jscpd + continue-on-error: true + shell: bash + run: | + T0=$(date +%s) + npx --yes jscpd \ + --silent \ + --threshold 0 \ + --min-tokens 70 \ + --reporters json \ + --output "${REPORTS}/jscpd" \ + --ignore "**/node_modules/**,**/dist/**,**/build/**,**/target/**,**/bin/**,**/obj/**,**/vendor/**,**/*.lock,**/*.min.*" \ + . > "${REPORTS}/jscpd-stdout.txt" 2>&1 || true + COUNT=$(jq '.statistics.total.duplicatedLines // 0' "${REPORTS}/jscpd/jscpd-report.json" 2>/dev/null || echo 0) + CLONES=$(jq '.statistics.total.clones // 0' "${REPORTS}/jscpd/jscpd-report.json" 2>/dev/null || echo 0) + echo "count=${CLONES}" >> "$GITHUB_OUTPUT" + echo "duplicated_lines=${COUNT}" >> "$GITHUB_OUTPUT" + echo "duration=$(( $(date +%s) - T0 ))" >> "$GITHUB_OUTPUT" + echo "jscpd clones: ${CLONES} (duplicated lines: ${COUNT})" + + - name: Upload SARIF to code scanning + if: ${{ always() && inputs.upload-sarif == 'true' }} + continue-on-error: true + uses: github/codeql-action/upload-sarif@v3 + with: + sarif_file: ${{ env.REPORTS }} + + - name: Claude review (optional) + id: claude + if: ${{ always() && github.event_name == 'pull_request' && inputs.run-claude != 'false' && inputs.anthropic-api-key != '' }} + shell: bash + env: + ANTHROPIC_API_KEY: ${{ inputs.anthropic-api-key }} + BASE_REF: ${{ github.base_ref }} + run: | + T0=$(date +%s) + npm --prefix "${{ github.action_path }}" install --silent --no-audit --no-fund + node "${{ github.action_path }}/scripts/claude-review.mjs" > "${REPORTS}/claude.json" + COUNT=$(jq 'length' "${REPORTS}/claude.json" 2>/dev/null || echo 0) + { + echo "count=${COUNT}" + echo "enabled=true" + echo "duration=$(( $(date +%s) - T0 ))" + } >> "$GITHUB_OUTPUT" + echo "Claude findings: ${COUNT}" + + - name: Notify (Discord) + if: ${{ always() && inputs.discord-webhook != '' }} + shell: bash + env: + DISCORD_WEBHOOK_URL: ${{ inputs.discord-webhook }} + REPORTS: ${{ env.REPORTS }} + GITLEAKS_COUNT: ${{ steps.gitleaks.outputs.count }} + GITLEAKS_DURATION: ${{ steps.gitleaks.outputs.duration }} + SEMGREP_COUNT: ${{ steps.semgrep.outputs.count }} + SEMGREP_DURATION: ${{ steps.semgrep.outputs.duration }} + TRIVY_COUNT: ${{ steps.trivy.outputs.count }} + TRIVY_VULN: ${{ steps.trivy.outputs.vuln }} + TRIVY_MISCONFIG: ${{ steps.trivy.outputs.misconfig }} + TRIVY_LICENSE: ${{ steps.trivy.outputs.license }} + TRIVY_DURATION: ${{ steps.trivy.outputs.duration }} + ESLINT_COUNT: ${{ steps.eslint.outputs.count }} + ESLINT_DURATION: ${{ steps.eslint.outputs.duration }} + RUFF_COUNT: ${{ steps.ruff.outputs.count }} + RUFF_DURATION: ${{ steps.ruff.outputs.duration }} + RUST_COUNT: ${{ steps.rust.outputs.count }} + RUST_DURATION: ${{ steps.rust.outputs.duration }} + DOTNET_COUNT: ${{ steps.dotnet.outputs.count }} + DOTNET_DURATION: ${{ steps.dotnet.outputs.duration }} + LIZARD_COUNT: ${{ steps.lizard.outputs.count }} + LIZARD_DURATION: ${{ steps.lizard.outputs.duration }} + JSCPD_COUNT: ${{ steps.jscpd.outputs.count }} + JSCPD_DUPLICATED_LINES: ${{ steps.jscpd.outputs.duplicated_lines }} + JSCPD_DURATION: ${{ steps.jscpd.outputs.duration }} + CLAUDE_COUNT: ${{ steps.claude.outputs.count }} + CLAUDE_ENABLED: ${{ steps.claude.outputs.enabled }} + CLAUDE_DURATION: ${{ steps.claude.outputs.duration }} + HAS_JS: ${{ steps.detect.outputs.has_js }} + HAS_PY: ${{ steps.detect.outputs.has_py }} + HAS_RUST: ${{ steps.detect.outputs.has_rust }} + HAS_DOTNET: ${{ steps.detect.outputs.has_dotnet }} + ESLINT_CFG: ${{ steps.detect.outputs.eslint_cfg }} + EVENT_NAME: ${{ github.event_name }} + PR_NUMBER: ${{ github.event.pull_request.number }} + PR_TITLE: ${{ github.event.pull_request.title }} + PR_URL: ${{ github.event.pull_request.html_url }} + COMMIT_SHA: ${{ github.sha }} + COMMIT_MESSAGE: ${{ github.event.head_commit.message }} + REPO: ${{ github.repository }} + ACTOR: ${{ github.actor }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: node "${{ github.action_path }}/scripts/notify.mjs" + + - name: Upload raw reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: securecheck-reports + path: ${{ env.REPORTS }} + if-no-files-found: ignore + retention-days: 14 + + - name: Severity gate + id: gate + if: always() + shell: bash + env: + FAIL_ON: ${{ inputs.fail-on }} + REPORTS: ${{ env.REPORTS }} + run: node "${{ github.action_path }}/scripts/gate.mjs"