| 1 | /** |
| 2 | * A repository's Security overview: one row per feature, each with a |
| 3 | * status word, a line of what it does, and the one thing to do about it. |
| 4 | * Pure, so the rows are tested apart from the page (routes/repo/security-overview.tsx). |
| 5 | */ |
| 6 | |
| 7 | export type FeatureStatus = "enabled" | "disabled" | "needs_setup" | "coming"; |
| 8 | |
| 9 | export const STATUS_LABEL: Record<FeatureStatus, string> = { |
| 10 | enabled: "Enabled", |
| 11 | disabled: "Disabled", |
| 12 | needs_setup: "Needs setup", |
| 13 | coming: "Coming", |
| 14 | }; |
| 15 | |
| 16 | /** The Badge tone each status word is shown in. */ |
| 17 | export const STATUS_TONE: Record<FeatureStatus, "success" | "neutral" | "warn" | "info"> = { |
| 18 | enabled: "success", |
| 19 | disabled: "neutral", |
| 20 | needs_setup: "warn", |
| 21 | coming: "info", |
| 22 | }; |
| 23 | |
| 24 | export type RowGroup = "policy" | "findings" | "reporting"; |
| 25 | |
| 26 | /** The one thing to do about a feature: a page to open, or a form to post. */ |
| 27 | export type RowAction = |
| 28 | | { kind: "link"; label: string; to: string } |
| 29 | | { kind: "post"; label: string; action: string; intent?: string; pending: string } |
| 30 | | { kind: "switch"; label: string; intent: string; on: boolean }; |
| 31 | |
| 32 | export type OverviewRow = { |
| 33 | key: string; |
| 34 | group: RowGroup; |
| 35 | title: string; |
| 36 | status: FeatureStatus; |
| 37 | about: string; |
| 38 | /** A short hint for the status, shown on hover, when the word alone is not enough. */ |
| 39 | hint?: string; |
| 40 | action: RowAction | null; |
| 41 | }; |
| 42 | |
| 43 | export type OverviewFacts = { |
| 44 | /** Under the project: `/acme/rocket`. */ |
| 45 | base: string; |
| 46 | /** Whether a SECURITY.md is on the default branch, and where. */ |
| 47 | policyPath: string | null; |
| 48 | /** Private vulnerability reporting. */ |
| 49 | reporting: boolean; |
| 50 | /** Published advisories. */ |
| 51 | advisories: number; |
| 52 | /** Security updates. */ |
| 53 | upkeep: boolean; |
| 54 | codeScanning: { configured: boolean; analyses: number; entitled: boolean }; |
| 55 | /** Alerts for secrets found in the history. Push protection is always on. */ |
| 56 | historyAlerts: boolean; |
| 57 | /** Whether the viewer may change settings (Admin), and push (for the policy's pull request). */ |
| 58 | canManage: boolean; |
| 59 | canPush: boolean; |
| 60 | }; |
| 61 | |
| 62 | /** Where a security policy may be: the first found is the one. */ |
| 63 | export const POLICY_PATHS = ["SECURITY.md", ".github/SECURITY.md", "docs/SECURITY.md"]; |
| 64 | |
| 65 | /** The rows, in the order the page shows them. */ |
| 66 | export function overviewRows(facts: OverviewFacts): OverviewRow[] { |
| 67 | const { base } = facts; |
| 68 | const code = facts.codeScanning; |
| 69 | return [ |
| 70 | { |
| 71 | key: "policy", |
| 72 | group: "policy", |
| 73 | title: "Security policy", |
| 74 | status: facts.policyPath ? "enabled" : "needs_setup", |
| 75 | about: facts.policyPath |
| 76 | ? `${facts.policyPath} on the default branch tells people how to report a vulnerability and which versions are supported.` |
| 77 | : "A SECURITY.md on the default branch tells people how to report a vulnerability and which versions are supported.", |
| 78 | action: facts.policyPath |
| 79 | ? { kind: "link", label: "View policy", to: `${base}/blob/HEAD/${facts.policyPath}` } |
| 80 | : facts.canPush |
| 81 | ? { kind: "post", label: "Set up a security policy", action: `${base}/security/policy/setup`, pending: "Opening a pull request…" } |
| 82 | : null, |
| 83 | }, |
| 84 | { |
| 85 | key: "advisories", |
| 86 | group: "reporting", |
| 87 | title: "Security advisories", |
| 88 | status: "enabled", |
| 89 | about: |
| 90 | facts.advisories === 0 |
| 91 | ? "Publish what this repository knows about vulnerabilities in what it ships, so people who depend on it can act." |
| 92 | : `${facts.advisories} published ${facts.advisories === 1 ? "advisory tells" : "advisories tell"} people who depend on this repository what to do.`, |
| 93 | action: { kind: "link", label: facts.canManage ? "Manage advisories" : "View advisories", to: `${base}/security/advisories` }, |
| 94 | }, |
| 95 | { |
| 96 | key: "reporting", |
| 97 | group: "reporting", |
| 98 | title: "Private vulnerability reporting", |
| 99 | status: facts.reporting ? "enabled" : "disabled", |
| 100 | about: facts.reporting |
| 101 | ? "Anyone who can see this repository can report a vulnerability to its maintainers privately, from its Security tab." |
| 102 | : "Let anyone who can see this repository report a vulnerability to its maintainers privately, instead of in an issue.", |
| 103 | action: facts.canManage ? { kind: "switch", label: "Private vulnerability reporting", intent: "reporting", on: facts.reporting } : null, |
| 104 | }, |
| 105 | { |
| 106 | key: "dependency_alerts", |
| 107 | group: "findings", |
| 108 | title: "Dependency alerts", |
| 109 | status: "enabled", |
| 110 | about: "Every package the lockfiles resolve is checked for known vulnerabilities, on every push to the default branch and daily.", |
| 111 | action: { kind: "link", label: "View alerts", to: `${base}/security/vulnerabilities` }, |
| 112 | }, |
| 113 | { |
| 114 | key: "security_updates", |
| 115 | group: "findings", |
| 116 | title: "Security updates", |
| 117 | status: facts.upkeep ? "enabled" : "disabled", |
| 118 | about: "A pull request upgrades each vulnerable dependency that has a fix; it lands through the branch's required checks.", |
| 119 | action: facts.canManage ? { kind: "switch", label: "Security updates", intent: "upkeep", on: facts.upkeep } : null, |
| 120 | }, |
| 121 | { |
| 122 | key: "code_scanning", |
| 123 | group: "findings", |
| 124 | title: "Code scanning", |
| 125 | status: code.analyses > 0 ? "enabled" : "needs_setup", |
| 126 | about: |
| 127 | code.analyses > 0 |
| 128 | ? "Your static analysis tools' results, uploaded as SARIF: alerts on the default branch and a check on pull requests." |
| 129 | : "Upload your static analysis tools' SARIF results from a workflow: alerts on the default branch and a check on pull requests.", |
| 130 | hint: !code.entitled ? "On a private repository, code scanning comes with the g1t plan." : undefined, |
| 131 | action: |
| 132 | code.analyses > 0 || code.configured |
| 133 | ? { kind: "link", label: "View alerts", to: `${base}/security/code-scanning` } |
| 134 | : facts.canManage && code.entitled |
| 135 | ? { kind: "post", label: "Set up code scanning", action: `${base}/security/code-scanning/setup`, pending: "Opening a pull request…" } |
| 136 | : { kind: "link", label: "Learn more", to: `${base}/security/code-scanning` }, |
| 137 | }, |
| 138 | { |
| 139 | key: "secret_scanning", |
| 140 | group: "findings", |
| 141 | title: "Secret scanning", |
| 142 | status: "enabled", |
| 143 | about: facts.historyAlerts |
| 144 | ? "Push protection refuses every push that adds a secret, and secrets already in the history open alerts." |
| 145 | : "Push protection refuses every push that adds a secret. Alerts for secrets already in the history are off.", |
| 146 | hint: "Push protection is always on.", |
| 147 | action: { kind: "link", label: "View alerts", to: `${base}/security/secret-scanning` }, |
| 148 | }, |
| 149 | { |
| 150 | key: "code_quality", |
| 151 | group: "findings", |
| 152 | title: "Code quality", |
| 153 | status: "coming", |
| 154 | about: "Maintainability and reliability findings beside the security ones. Not built yet.", |
| 155 | action: null, |
| 156 | }, |
| 157 | ]; |
| 158 | } |
| 159 | |
| 160 | /** The rows of one group, in order. */ |
| 161 | export function rowsIn(rows: OverviewRow[], group: RowGroup): OverviewRow[] { |
| 162 | return rows.filter((row) => row.group === group); |
| 163 | } |
| 164 | |
| 165 | /** |
| 166 | * The SECURITY.md "Set up a security policy" proposes: how to report, and |
| 167 | * which versions are supported, for the maintainers to finish. |
| 168 | */ |
| 169 | export function securityPolicyTemplate(repo: string, reporting: boolean, base: string): string { |
| 170 | const report = reporting |
| 171 | ? `Report it privately at https://g1t.sh${base}/security/report. You can report without an account; leave an email address so the maintainers can answer you.` |
| 172 | : `Email the maintainers (fill in the address here) rather than opening an issue, so the problem is not public before a fix is.`; |
| 173 | return `# Security policy |
| 174 | |
| 175 | ## Reporting a vulnerability |
| 176 | |
| 177 | If you find a vulnerability in ${repo}, do not open an issue about it. |
| 178 | |
| 179 | ${report} |
| 180 | |
| 181 | Say what is affected, how to reproduce it and, if you can, how bad you think it is. We aim to answer within a few days and will keep you informed as we work on a fix. If you want to be credited in the advisory, say so. |
| 182 | |
| 183 | ## Supported versions |
| 184 | |
| 185 | | Version | Supported | |
| 186 | | ------- | --------- | |
| 187 | | latest | Yes | |
| 188 | |
| 189 | Fill in which releases get security fixes. |
| 190 | `; |
| 191 | } |
| 192 | |
| 193 | /** The pull request's body for the policy. */ |
| 194 | export function securityPolicyPullBody(path: string): string { |
| 195 | return `Adds \`${path}\`, which tells people how to report a vulnerability and which versions get security fixes. Edit the address and the versions before merging.`; |
| 196 | } |
| 197 | |
| 198 | /** A branch name for the policy that is not taken. */ |
| 199 | export function securityPolicyBranch(taken: string[]): string { |
| 200 | let name = "add-security-policy"; |
| 201 | for (let n = 2; taken.includes(name); n++) name = `add-security-policy-${n}`; |
| 202 | return name; |
| 203 | } |