-
Notifications
You must be signed in to change notification settings - Fork 0
Security Guardrails
Passive safety nets in this repo. Everything on this page is automated so the security posture doesn't depend on any human noticing an event in time.
The full policy lives in SECURITY.md at the repo root.
Highlights:
- Preferred report channel is a private GitHub security advisory. Email fallback is
mdeguzis@gmail.com. - Response SLA: acknowledge within 72 hours, initial assessment within one week.
- Coordinated disclosure default: 90 days, negotiable.
- Safe-harbor language for good-faith research.
- The same policy mirrors to
decky-proton-pulse/SECURITY.mdso researchers land in one place regardless of which repo they were poking at.
Pinning tests live in tests/securityMd.test.js so a future edit that drops scope, SLA, or safe-harbor language fails CI.
Runs on every push + pull request against main / staging, plus a weekly cron. Uses three rulepacks:
-
p/typescript-- TS-specific issues (unsafeanycasts, unhandled rejects, tainted-flow through the Steam Web API proxies). -
p/owasp-top-ten-- XSS sinks, SQLi patterns, SSRF, auth bypass, cookie flag misuse. -
p/security-audit-- hardcoded credentials, weak crypto.
Complements CodeQL: CodeQL only fully analyzes our JS, so Semgrep is the SAST coverage for the Deno TypeScript inside supabase/functions/. New ERROR-severity finding = merge blocked. False positives get a scoped # nosemgrep: rule-id ignore comment with a written justification.
Workflow: .github/workflows/semgrep.yml.
Every push generates a CycloneDX SBOM via Syft and scans it against NVD + GitHub Security Advisories via Grype. Complements npm audit (which only covers the npm advisory DB) by adding coverage for bundled / transitive / CDN-loaded dependencies.
- Fails the build on any high or critical finding (same policy as
npm audit). - SBOM uploaded as a workflow artifact; retained 90 days for post-hoc "what did we ship?" audits.
- GitHub consumes CycloneDX SBOMs natively via the dependency graph, so this also feeds Dependabot with richer signal.
Workflow: .github/workflows/sbom.yml.
The aggregate 429 check in security-monitor.mjs catches site-wide storms but misses one persistent attacker who trips the limit repeatedly from the same source. A per-IP variant runs in the same hourly pass, groups last-hour 429s by client IP, and files a security-labeled issue if any single IP crosses RATE_LIMIT_ABUSE_THRESHOLD (currently 50 trips/hour).
The existing discord-issues.yml workflow picks up new issues automatically, so no separate Discord webhook is needed for the alert to land in the security channel. When the alert fires, the issue body lists the top 10 offenders with their trip counts so the response is a WAF or DNS-provider block, not a code change.
Script: .github/scripts/security-monitor.mjs.
Backups are signed and pushed to mdeguzis/proton-pulse-data-backup on every schema change (see Restore-Runbook). Backups that have never been restored are hope, not recovery. A scheduled workflow files a fresh drill-checklist issue every 90 days (1st of Jan / Apr / Jul / Oct) so the restore path stays proven against a scratch Supabase project. Closing the issue records the drill outcome.
Workflow: .github/workflows/restore-drill-reminder.yml.
Every HTML page ships a meta Content-Security-Policy tag. Adding a new remote origin (script, connect-src, img-src) requires updating the tag on the pages that call it. Current non-obvious entries:
| Page | connect-src origin | Why |
|---|---|---|
status.html |
https://www.githubstatus.com |
vendor status card polls GitHub Pages + Actions component health |
status.html |
https://www.cloudflarestatus.com |
vendor status card polls Cloudflare Workers, KV, CDN, DNS health |
status.html |
https://pp-edge-status.mdeguzis.workers.dev |
Cloudflare Worker that caches edge-fn health signal |
| all pages | https://ilsgdshkaocrmibwdezk.supabase.co |
Supabase REST + edge functions |
Regression tests pin the vendor status origins so a future CSP edit that drops one fails locally instead of silently in prod:
-
tests/statusCsp.test.jscoversstatus.html
What it does. Every issues.opened, issue_comment.created, and pull_request_review_comment.created event fires a workflow that:
- Extracts every GitHub user-attachment URL from the body.
- HEADs each URL and downloads (up to 16 MB) to hash it.
- Sends the SHA256 to VirusTotal's
/api/v3/files/{sha256}endpoint. Hash-only, no upload of content. - Flags any attachment where:
- VirusTotal has one or more malicious engine hits, OR
- The file starts with a Windows PE header (
MZ) regardless of extension, OR - The extension is in the known-executable set (
.exe,.msi,.scr,.ps1,.jar,.apk,.dmg,.sh, ...).
- On a flag:
- Hides the offending comment via GraphQL
minimizeComment(classifier:ABUSE). - Applies the
security-reviewlabel to the issue. - Posts a summary on the issue pinging the repo owner with SHA256s and reason strings.
- Hides the offending comment via GraphQL
The repo owner's own attachments are exempt (github.actor != github.repository_owner) so the workflow can't lock you out of your own repo.
What it does NOT do.
- Never uploads issue content anywhere. VirusTotal receives only a hash.
- Never executes the file. Analysis is limited to header sniffing + hash lookup.
- Doesn't touch existing accepted attachments; only fires on new / edited posts.
- Doesn't block first-time contributors on legitimate content (screenshots, logs, plain-text traces, PDFs, archives -- all left alone).
Files.
-
.github/workflows/scan-issue-attachments.yml-- trigger + permissions. -
.github/scripts/scan-attachments.mjs-- runtime logic (HTTP, GraphQL, decisions). -
.github/scripts/scan-attachments-lib.mjs-- pure helpers, unit-tested. -
tests/scanAttachments.test.js-- pins URL extraction, extension policy, VirusTotal response parsing, and the workflow YAML contract.
Secret required.
-
VT_API_KEY-- VirusTotal API key. Free tier gives 500 hash lookups per day, which is far above the traffic this project sees. See Secrets and Environment for how to set it and where to get one.
If VT_API_KEY is unset, the workflow degrades gracefully: it still applies the extension-only policy (blocking bare .exe etc. from non-owners), but skips VirusTotal lookups.
Signature of an incident. The wapebacoko submission on issue #226 is the reference example:
- 2.9 MB
metadata_fix_v2.exe. - Static analysis showed a Windows credential stealer (Go-compiled,
LogonUserW,AdjustTokenPrivileges,NetUserAdd,DuplicateTokenEximports, obfuscated main package name, zeroed compile timestamp). - SHA256:
d85d164e46fabb085609f2586e8fec364539a6ec81f74659f0cb28ac76e7880b.
The scanner is designed to catch that exact class of upload: extension-only alone would flag it, and VirusTotal has multiple engines that recognize the payload.
False positives. If the scanner hides a legitimate attachment:
- Re-open the hidden comment via the GitHub comment
...menu. - Remove the
security-reviewlabel from the issue. - If the attachment was a legitimate non-owner upload of an executable, add a note to the issue explaining why it was expected.
- Consider adding the URL host or an extension exemption in
scan-attachments-lib.mjsif the same false positive pattern shows up more than once.
The hide is always reversible; there is no automated ban or ownership removal.
Related.
- Memory rule "never extract/execute untrusted GH issue attachments" (session policy for me).
- Issue #225 (resilience -- same "don't rely on humans catching every issue" theme).
-
Secrets-and-Environment -- where
VT_API_KEYlives.