Skip to content

Commit

The API carries the advisory and report operations the service gained, with the two new repository security settings (history_alerts, private_vulnerability_reporting) on get_security_settings and update_security_settings, and the docs' OpenAPI copy is regenerated to match.

syntaqxcommitted Parent02b5e74Browse files
7 files+565−70/7 viewed
+14−0
285285 ],
286286 ),
287287 (
288+ "Security advisories",
289+ "What a repository says about vulnerabilities in what it ships: advisories drafted, published and withdrawn by its maintainers, and the vulnerability reports people send them privately when the repository takes them.",
290+ &[
291+ Op::Security(SecurityOp::ListAdvisories),
292+ Op::Security(SecurityOp::CreateAdvisory),
293+ Op::Security(SecurityOp::GetAdvisory),
294+ Op::Security(SecurityOp::UpdateAdvisory),
295+ Op::Security(SecurityOp::SetAdvisoryState),
296+ Op::Security(SecurityOp::ListReports),
297+ Op::Security(SecurityOp::ReportVulnerability),
298+ Op::Security(SecurityOp::ReviewReport),
299+ ],
300+ ),
301+ (
288302 "Rules",
289303 "Rulesets: what may happen to a repository's branches and tags and what a pull request needs before it merges, for a repository or across a workspace; the rules that hold for one branch; and how they judged each push and merge, with insights.",
290304 &[
+9−1
711711 }
712712
713713 impl Op {
714− pub const ALL: [Op; 371] = [
714+ pub const ALL: [Op; 379] = [
715715 Op::Whoami,
716716 Op::GetWorkspace,
717717 Op::CreateWorkspace,
940940 Op::Security(SecurityOp::GetWorkspaceSettings),
941941 Op::Security(SecurityOp::UpdateWorkspaceSettings),
942942 Op::Security(SecurityOp::GetOverview),
943+ Op::Security(SecurityOp::ListAdvisories),
944+ Op::Security(SecurityOp::GetAdvisory),
945+ Op::Security(SecurityOp::CreateAdvisory),
946+ Op::Security(SecurityOp::UpdateAdvisory),
947+ Op::Security(SecurityOp::SetAdvisoryState),
948+ Op::Security(SecurityOp::ListReports),
949+ Op::Security(SecurityOp::ReportVulnerability),
950+ Op::Security(SecurityOp::ReviewReport),
943951 Op::Rules(RulesOp::ListRepoRulesets),
944952 Op::Rules(RulesOp::GetRepoRuleset),
945953 Op::Rules(RulesOp::CreateRepoRuleset),
+312−2
95039503 "review_deny_licenses": [
95049504 "AGPL-3.0-only"
95059505 ],
9506− "review_comment": true
9506+ "review_comment": true,
9507+ "history_alerts": true,
9508+ "private_vulnerability_reporting": false
95079509 },
95089510 "workspace": {
95099511 "delegated_bypass": true,
95339535 "review_deny_licenses": [
95349536 "AGPL-3.0-only"
95359537 ],
9536− "review_comment": true
9538+ "review_comment": true,
9539+ "history_alerts": true,
9540+ "private_vulnerability_reporting": true
95379541 },
95389542 "workspace": {
95399543 "delegated_bypass": true,
95729576 "activated": true
95739577 }
95749578 },
9579+ "list_security_advisories": {
9580+ "params": {
9581+ "owner": "flagon-io",
9582+ "name": "hello"
9583+ },
9584+ "query": {
9585+ "state": "published"
9586+ },
9587+ "response": [
9588+ {
9589+ "id": "adv_01kq2p9f6g7h8j9k0m1n2p3q4r",
9590+ "g1tsa_id": "G1TSA-2026-h7kd-3mqw",
9591+ "repo_id": "rep_01kq2p9f6g7h8j9k0m1n2p3q4r",
9592+ "repo": "flagon-io/hello",
9593+ "title": "Path traversal in the archive reader",
9594+ "description": "A crafted archive could write outside the directory it is extracted to.\n\nUpgrade to 1.4.1.",
9595+ "severity": "high",
9596+ "cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N",
9597+ "cvss_score": 8.1,
9598+ "cve_id": "CVE-2026-12345",
9599+ "ecosystem": "npm",
9600+ "package": "@flagon/hello",
9601+ "affected_versions": ">= 1.2.0, < 1.4.1",
9602+ "patched_versions": ">= 1.4.1",
9603+ "state": "published",
9604+ "credits": [
9605+ "ana"
9606+ ],
9607+ "report_id": "vrp_01kq2p9f6g7h8j9k0m1n2p3q4r",
9608+ "created_by": "syntaqx",
9609+ "created_at": "2026-10-06T09:14:02.118Z",
9610+ "updated_at": "2026-10-07T15:02:40.000Z",
9611+ "published_at": "2026-10-07T15:02:40.000Z",
9612+ "withdrawn_at": null
9613+ }
9614+ ],
9615+ "notes": "Without a token, or with one that has no Write on the repository, only published advisories are listed. `state` filters to `draft`, `published` or `withdrawn`; drafts and withdrawn advisories are shown only to those who see the repository's findings."
9616+ },
9617+ "get_security_advisory": {
9618+ "params": {
9619+ "owner": "flagon-io",
9620+ "name": "hello",
9621+ "id": "G1TSA-2026-h7kd-3mqw"
9622+ },
9623+ "response": {
9624+ "id": "adv_01kq2p9f6g7h8j9k0m1n2p3q4r",
9625+ "g1tsa_id": "G1TSA-2026-h7kd-3mqw",
9626+ "repo_id": "rep_01kq2p9f6g7h8j9k0m1n2p3q4r",
9627+ "repo": "flagon-io/hello",
9628+ "title": "Path traversal in the archive reader",
9629+ "description": "A crafted archive could write outside the directory it is extracted to.\n\nUpgrade to 1.4.1.",
9630+ "severity": "high",
9631+ "cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N",
9632+ "cvss_score": 8.1,
9633+ "cve_id": "CVE-2026-12345",
9634+ "ecosystem": "npm",
9635+ "package": "@flagon/hello",
9636+ "affected_versions": ">= 1.2.0, < 1.4.1",
9637+ "patched_versions": ">= 1.4.1",
9638+ "state": "published",
9639+ "credits": [
9640+ "ana"
9641+ ],
9642+ "report_id": "vrp_01kq2p9f6g7h8j9k0m1n2p3q4r",
9643+ "created_by": "syntaqx",
9644+ "created_at": "2026-10-06T09:14:02.118Z",
9645+ "updated_at": "2026-10-07T15:02:40.000Z",
9646+ "published_at": "2026-10-07T15:02:40.000Z",
9647+ "withdrawn_at": null
9648+ },
9649+ "notes": "`id` is the advisory's `adv_…` id or its `G1TSA-…` id. A draft or withdrawn advisory is `404` to anyone who does not see the repository's findings."
9650+ },
9651+ "create_security_advisory": {
9652+ "params": {
9653+ "owner": "flagon-io",
9654+ "name": "hello"
9655+ },
9656+ "request": {
9657+ "title": "Path traversal in the archive reader",
9658+ "description": "A crafted archive could write outside the directory it is extracted to.",
9659+ "severity": "high",
9660+ "ecosystem": "npm",
9661+ "package": "@flagon/hello",
9662+ "affected_versions": ">= 1.2.0, < 1.4.1",
9663+ "patched_versions": ">= 1.4.1",
9664+ "credits": [
9665+ "ana"
9666+ ]
9667+ },
9668+ "response": {
9669+ "id": "adv_01kq2p9f6g7h8j9k0m1n2p3q4r",
9670+ "g1tsa_id": "G1TSA-2026-h7kd-3mqw",
9671+ "repo_id": "rep_01kq2p9f6g7h8j9k0m1n2p3q4r",
9672+ "repo": "flagon-io/hello",
9673+ "title": "Path traversal in the archive reader",
9674+ "description": "A crafted archive could write outside the directory it is extracted to.",
9675+ "severity": "high",
9676+ "cvss_vector": null,
9677+ "cvss_score": null,
9678+ "cve_id": null,
9679+ "ecosystem": "npm",
9680+ "package": "@flagon/hello",
9681+ "affected_versions": ">= 1.2.0, < 1.4.1",
9682+ "patched_versions": ">= 1.4.1",
9683+ "state": "draft",
9684+ "credits": [
9685+ "ana"
9686+ ],
9687+ "report_id": null,
9688+ "created_by": "syntaqx",
9689+ "created_at": "2026-10-06T09:14:02.118Z",
9690+ "updated_at": "2026-10-06T09:14:02.118Z",
9691+ "published_at": null,
9692+ "withdrawn_at": null
9693+ },
9694+ "notes": "The advisory starts as a draft nobody but the maintainers sees; publish it with `POST …/advisories/{id}/state`. Takes the Admin role, or a security manager of the workspace. A title or severity that is missing, or a `cve_id` that is not of the form `CVE-2026-12345`, is `422`."
9695+ },
9696+ "update_security_advisory": {
9697+ "params": {
9698+ "owner": "flagon-io",
9699+ "name": "hello",
9700+ "id": "adv_01kq2p9f6g7h8j9k0m1n2p3q4r"
9701+ },
9702+ "request": {
9703+ "cve_id": "CVE-2026-12345",
9704+ "cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N",
9705+ "cvss_score": 8.1
9706+ },
9707+ "response": {
9708+ "id": "adv_01kq2p9f6g7h8j9k0m1n2p3q4r",
9709+ "g1tsa_id": "G1TSA-2026-h7kd-3mqw",
9710+ "repo_id": "rep_01kq2p9f6g7h8j9k0m1n2p3q4r",
9711+ "repo": "flagon-io/hello",
9712+ "title": "Path traversal in the archive reader",
9713+ "description": "A crafted archive could write outside the directory it is extracted to.",
9714+ "severity": "high",
9715+ "cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N",
9716+ "cvss_score": 8.1,
9717+ "cve_id": "CVE-2026-12345",
9718+ "ecosystem": "npm",
9719+ "package": "@flagon/hello",
9720+ "affected_versions": ">= 1.2.0, < 1.4.1",
9721+ "patched_versions": ">= 1.4.1",
9722+ "state": "draft",
9723+ "credits": [
9724+ "ana"
9725+ ],
9726+ "report_id": null,
9727+ "created_by": "syntaqx",
9728+ "created_at": "2026-10-06T09:14:02.118Z",
9729+ "updated_at": "2026-10-06T10:30:00.000Z",
9730+ "published_at": null,
9731+ "withdrawn_at": null
9732+ },
9733+ "notes": "What the request leaves out stays as it is. A published advisory may be changed; its page shows the change at once."
9734+ },
9735+ "set_security_advisory_state": {
9736+ "params": {
9737+ "owner": "flagon-io",
9738+ "name": "hello",
9739+ "id": "adv_01kq2p9f6g7h8j9k0m1n2p3q4r"
9740+ },
9741+ "request": {
9742+ "state": "published"
9743+ },
9744+ "response": {
9745+ "id": "adv_01kq2p9f6g7h8j9k0m1n2p3q4r",
9746+ "g1tsa_id": "G1TSA-2026-h7kd-3mqw",
9747+ "repo_id": "rep_01kq2p9f6g7h8j9k0m1n2p3q4r",
9748+ "repo": "flagon-io/hello",
9749+ "title": "Path traversal in the archive reader",
9750+ "description": "A crafted archive could write outside the directory it is extracted to.",
9751+ "severity": "high",
9752+ "cvss_vector": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:N",
9753+ "cvss_score": 8.1,
9754+ "cve_id": "CVE-2026-12345",
9755+ "ecosystem": "npm",
9756+ "package": "@flagon/hello",
9757+ "affected_versions": ">= 1.2.0, < 1.4.1",
9758+ "patched_versions": ">= 1.4.1",
9759+ "state": "published",
9760+ "credits": [
9761+ "ana"
9762+ ],
9763+ "report_id": null,
9764+ "created_by": "syntaqx",
9765+ "created_at": "2026-10-06T09:14:02.118Z",
9766+ "updated_at": "2026-10-07T15:02:40.000Z",
9767+ "published_at": "2026-10-07T15:02:40.000Z",
9768+ "withdrawn_at": null
9769+ },
9770+ "notes": "Publishing a draft with no description is `422`. Publishing sends `security_advisory.published` to webhooks and tells the repository's watchers who chose security alerts; withdrawing (`state` `withdrawn`) sends `security_advisory.withdrawn`. A withdrawn advisory can be published again. Asking for the state it already has is `409`."
9771+ },
9772+ "list_vulnerability_reports": {
9773+ "params": {
9774+ "owner": "flagon-io",
9775+ "name": "hello"
9776+ },
9777+ "query": {
9778+ "state": "new"
9779+ },
9780+ "response": [
9781+ {
9782+ "id": "vrp_01kq2p9f6g7h8j9k0m1n2p3q4r",
9783+ "repo_id": "rep_01kq2p9f6g7h8j9k0m1n2p3q4r",
9784+ "repo": "flagon-io/hello",
9785+ "reporter": null,
9786+ "reporter_email": "finder@example.com",
9787+ "title": "Path traversal in the archive reader",
9788+ "description": "Extracting an archive with an entry named ../../etc/passwd writes outside the target directory. Steps: …",
9789+ "severity": "high",
9790+ "state": "new",
9791+ "advisory_id": null,
9792+ "created_at": "2026-10-06T08:40:11.000Z",
9793+ "reviewed_by": null,
9794+ "reviewed_at": null,
9795+ "review_note": null
9796+ }
9797+ ],
9798+ "notes": "New reports first. Those who see the repository's findings (Write, or a security manager) read them; a reporter never sees another's. `state` is `new`, `accepted` or `declined`."
9799+ },
9800+ "report_vulnerability": {
9801+ "params": {
9802+ "owner": "flagon-io",
9803+ "name": "hello"
9804+ },
9805+ "request": {
9806+ "title": "Path traversal in the archive reader",
9807+ "description": "Extracting an archive with an entry named ../../etc/passwd writes outside the target directory. Steps: …",
9808+ "severity": "high",
9809+ "email": "finder@example.com"
9810+ },
9811+ "response": {
9812+ "id": "vrp_01kq2p9f6g7h8j9k0m1n2p3q4r",
9813+ "repo_id": "rep_01kq2p9f6g7h8j9k0m1n2p3q4r",
9814+ "repo": "flagon-io/hello",
9815+ "reporter": null,
9816+ "reporter_email": "finder@example.com",
9817+ "title": "Path traversal in the archive reader",
9818+ "description": "Extracting an archive with an entry named ../../etc/passwd writes outside the target directory. Steps: …",
9819+ "severity": "high",
9820+ "state": "new",
9821+ "advisory_id": null,
9822+ "created_at": "2026-10-06T08:40:11.000Z",
9823+ "reviewed_by": null,
9824+ "reviewed_at": null,
9825+ "review_note": null
9826+ },
9827+ "notes": "Needs private vulnerability reporting on for the repository (`409` otherwise). With a token, the report is yours by username; without one, on a public repository, give `email`, or the request is `422`. The workspace's owners and security managers are told at once. More than 5 reports a day from one reporter to one repository, or a repository with 50 new reports waiting, is refused."
9828+ },
9829+ "review_vulnerability_report": {
9830+ "params": {
9831+ "owner": "flagon-io",
9832+ "name": "hello",
9833+ "id": "vrp_01kq2p9f6g7h8j9k0m1n2p3q4r"
9834+ },
9835+ "request": {
9836+ "decision": "accept",
9837+ "note": "Confirmed on 1.4.0."
9838+ },
9839+ "response": {
9840+ "report": {
9841+ "id": "vrp_01kq2p9f6g7h8j9k0m1n2p3q4r",
9842+ "repo_id": "rep_01kq2p9f6g7h8j9k0m1n2p3q4r",
9843+ "repo": "flagon-io/hello",
9844+ "reporter": null,
9845+ "reporter_email": "finder@example.com",
9846+ "title": "Path traversal in the archive reader",
9847+ "description": "Extracting an archive with an entry named ../../etc/passwd writes outside the target directory. Steps: …",
9848+ "severity": "high",
9849+ "state": "accepted",
9850+ "advisory_id": "adv_01kq2p9f6g7h8j9k0m1n2p3q4r",
9851+ "created_at": "2026-10-06T08:40:11.000Z",
9852+ "reviewed_by": "syntaqx",
9853+ "reviewed_at": "2026-10-06T09:14:02.118Z",
9854+ "review_note": "Confirmed on 1.4.0."
9855+ },
9856+ "advisory": {
9857+ "id": "adv_01kq2p9f6g7h8j9k0m1n2p3q4r",
9858+ "g1tsa_id": "G1TSA-2026-h7kd-3mqw",
9859+ "repo_id": "rep_01kq2p9f6g7h8j9k0m1n2p3q4r",
9860+ "repo": "flagon-io/hello",
9861+ "title": "Path traversal in the archive reader",
9862+ "description": "Extracting an archive with an entry named ../../etc/passwd writes outside the target directory. Steps: …",
9863+ "severity": "high",
9864+ "cvss_vector": null,
9865+ "cvss_score": null,
9866+ "cve_id": null,
9867+ "ecosystem": null,
9868+ "package": null,
9869+ "affected_versions": null,
9870+ "patched_versions": null,
9871+ "state": "draft",
9872+ "credits": [
9873+ "finder@example.com"
9874+ ],
9875+ "report_id": "vrp_01kq2p9f6g7h8j9k0m1n2p3q4r",
9876+ "created_by": "syntaqx",
9877+ "created_at": "2026-10-06T09:14:02.118Z",
9878+ "updated_at": "2026-10-06T09:14:02.118Z",
9879+ "published_at": null,
9880+ "withdrawn_at": null
9881+ }
9882+ },
9883+ "notes": "`accept` makes a draft advisory from the report, in the report's words and crediting the reporter, for the maintainers to finish and publish; `advisory` is that draft. `decline` closes the report; `advisory` is null. A report that was reviewed already is `409`. Takes the Admin role, or a security manager of the workspace."
9884+ },
95759885 "get_security_overview": {
95769886 "params": {
95779887 "workspace": "flagon-io"
+9−0
256256 route("GET", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::GetWorkspaceSettings), &[]),
257257 route("PATCH", "/workspaces/:workspace/security/settings", Op::Security(SecurityOp::UpdateWorkspaceSettings), &[]),
258258 route("GET", "/workspaces/:workspace/security/overview", Op::Security(SecurityOp::GetOverview), &[("days", "days")]),
259+ // Reporting: a repository's advisories, and the vulnerability reports sent to its maintainers.
260+ route("GET", "/repos/:owner/:name/security/advisories", Op::Security(SecurityOp::ListAdvisories), &[("state", "state")]),
261+ route("POST", "/repos/:owner/:name/security/advisories", Op::Security(SecurityOp::CreateAdvisory), &[]),
262+ route("GET", "/repos/:owner/:name/security/advisories/:id", Op::Security(SecurityOp::GetAdvisory), &[]),
263+ route("PATCH", "/repos/:owner/:name/security/advisories/:id", Op::Security(SecurityOp::UpdateAdvisory), &[]),
264+ route("POST", "/repos/:owner/:name/security/advisories/:id/state", Op::Security(SecurityOp::SetAdvisoryState), &[]),
265+ route("GET", "/repos/:owner/:name/security/reports", Op::Security(SecurityOp::ListReports), &[("state", "state")]),
266+ route("POST", "/repos/:owner/:name/security/reports", Op::Security(SecurityOp::ReportVulnerability), &[]),
267+ route("PATCH", "/repos/:owner/:name/security/reports/:id", Op::Security(SecurityOp::ReviewReport), &[]),
259268 route("GET", "/repos", Op::ListRepos, &[("q", "query")]),
260269 route(
261270 "GET",
+212−3
1212 //! need the g1t plan, which Security and quality comes with (a 402 says so); this module
1313 //! reads the input and gives each answer its public shape.
1414
15+use g1t_contracts::advisories::{
16+ Advisory, AdvisoryInput, CreateAdvisoryArgs, GetAdvisoryArgs, ListAdvisoriesArgs, ListReportsArgs, ReportVulnerabilityArgs, ReviewReportArgs,
17+ SetAdvisoryStateArgs, UpdateAdvisoryArgs,
18+};
1519 use g1t_contracts::repos::RepoPath;
1620 use g1t_contracts::security::{AlertChange, AlertState, DismissArgs, DismissReason, ReopenArgs, SecretFinding, SecurityOverview, Vulnerability};
1721 use g1t_contracts::security_suite::*;
5761 GetWorkspaceSettings,
5862 UpdateWorkspaceSettings,
5963 GetOverview,
64+ ListAdvisories,
65+ GetAdvisory,
66+ CreateAdvisory,
67+ UpdateAdvisory,
68+ SetAdvisoryState,
69+ ListReports,
70+ ReportVulnerability,
71+ ReviewReport,
6072 }
6173
6274 impl SecurityOp {
6375 /// Every one: `Op::ALL` lists each as `Op::Security(…)`, which a test
6476 /// checks against this.
6577 #[cfg(test)]
66− pub const ALL: [SecurityOp; 31] = [
78+ pub const ALL: [SecurityOp; 39] = [
6779 SecurityOp::ListSecretAlerts,
6880 SecurityOp::GetSecretAlert,
6981 SecurityOp::UpdateSecretAlert,
95107 SecurityOp::GetWorkspaceSettings,
96108 SecurityOp::UpdateWorkspaceSettings,
97109 SecurityOp::GetOverview,
110+ SecurityOp::ListAdvisories,
111+ SecurityOp::GetAdvisory,
112+ SecurityOp::CreateAdvisory,
113+ SecurityOp::UpdateAdvisory,
114+ SecurityOp::SetAdvisoryState,
115+ SecurityOp::ListReports,
116+ SecurityOp::ReportVulnerability,
117+ SecurityOp::ReviewReport,
98118 ];
99119
100120 pub fn name(self) -> &'static str {
130150 SecurityOp::GetWorkspaceSettings => "get_workspace_security_settings",
131151 SecurityOp::UpdateWorkspaceSettings => "update_workspace_security_settings",
132152 SecurityOp::GetOverview => "get_security_overview",
153+ SecurityOp::ListAdvisories => "list_security_advisories",
154+ SecurityOp::GetAdvisory => "get_security_advisory",
155+ SecurityOp::CreateAdvisory => "create_security_advisory",
156+ SecurityOp::UpdateAdvisory => "update_security_advisory",
157+ SecurityOp::SetAdvisoryState => "set_security_advisory_state",
158+ SecurityOp::ListReports => "list_vulnerability_reports",
159+ SecurityOp::ReportVulnerability => "report_vulnerability",
160+ SecurityOp::ReviewReport => "review_vulnerability_report",
133161 }
134162 }
135163
167195 SecurityOp::GetWorkspaceSettings => "Get a workspace's security settings",
168196 SecurityOp::UpdateWorkspaceSettings => "Update a workspace's security settings",
169197 SecurityOp::GetOverview => "Get the security overview",
198+ SecurityOp::ListAdvisories => "List security advisories",
199+ SecurityOp::GetAdvisory => "Get a security advisory",
200+ SecurityOp::CreateAdvisory => "Create a security advisory",
201+ SecurityOp::UpdateAdvisory => "Update a security advisory",
202+ SecurityOp::SetAdvisoryState => "Publish or withdraw a security advisory",
203+ SecurityOp::ListReports => "List vulnerability reports",
204+ SecurityOp::ReportVulnerability => "Report a vulnerability privately",
205+ SecurityOp::ReviewReport => "Accept or decline a vulnerability report",
170206 }
171207 }
172208
198234 SecurityOp::GetDependencyGraph => "Get the dependency graph: every package the lockfiles on the default branch resolve, per lockfile, with whether it is direct or transitive (where the lockfile says), for development, its license when recorded, its package URL and its open vulnerability alerts.",
199235 SecurityOp::GetSbom => "Export the dependency graph as an SPDX 2.3 JSON document, in sbom. Every package is named by its package URL.",
200236 SecurityOp::CompareDependencies => "Compare the dependencies at two commits, branches or tags (basehead, as base...head): what was added and removed per lockfile, with the known vulnerabilities of what was added and whether it passes the repository's dependency review policy. Needs the g1t plan on a private repository.",
201− SecurityOp::GetSettings => "Get a repository's security settings: when the Code scanning check fails, dependency review and its policy, its workspace's settings, and whether the paid features are on for it.",
202− SecurityOp::UpdateSettings => "Change a repository's security settings: code_scanning_gate (none, errors, critical, high, medium or any), dependency_review, review_fail_on (critical, high, medium, low or none), review_deny_licenses (SPDX ids) and review_comment. Takes the Admin role, or a security manager of the workspace. Require the Code scanning and Dependency review checks in branch protection to gate merges on them.",
237+ SecurityOp::GetSettings => "Get a repository's security settings: when the Code scanning check fails, dependency review and its policy, whether secrets in the history open alerts, whether it takes private vulnerability reports, its workspace's settings, and whether the paid features are on for it.",
238+ SecurityOp::UpdateSettings => "Change a repository's security settings: code_scanning_gate (none, errors, critical, high, medium or any), dependency_review, review_fail_on (critical, high, medium, low or none), review_deny_licenses (SPDX ids), review_comment, history_alerts (whether secrets found in the history open alerts; push protection stays on) and private_vulnerability_reporting. Takes the Admin role, or a security manager of the workspace. Require the Code scanning and Dependency review checks in branch protection to gate merges on them.",
203239 SecurityOp::GetWorkspaceSettings => "Get a workspace's security settings (delegated bypass, validity checks) and whether it has Security and quality, which comes with the g1t plan.",
204240 SecurityOp::UpdateWorkspaceSettings => "Turn delegated bypass and validity checks on or off for a workspace. Owners only.",
205241 SecurityOp::GetOverview => "Get a workspace's security overview: open alerts by type and severity, how many opened and closed in the last days (7 to 90, 30 by default), a daily trend, and for each repository which features are on and what is open, most in need first. Private repositories count only on the g1t plan.",
242+ SecurityOp::ListAdvisories => "List a repository's security advisories, newest first: what it says about vulnerabilities in what it ships. Anyone who can see the repository sees the published ones; those who see its findings (Write, or a security manager) see drafts and withdrawn ones too. Filter by state (draft, published, withdrawn).",
243+ SecurityOp::GetAdvisory => "Get one security advisory by its adv_… id or its G1TSA-… id, with the same visibility as the list.",
244+ SecurityOp::CreateAdvisory => "Create a draft security advisory: title, description (Markdown), severity (critical, high, medium or low), and optionally cvss_vector and cvss_score, cve_id, the package affected (ecosystem, package, affected_versions, patched_versions) and credits. Admin on the repository, or a security manager of its workspace. Nobody is told of a draft.",
245+ SecurityOp::UpdateAdvisory => "Change an advisory's fields, in any state; what is not given stays as it is. A published advisory's change is visible at once. Admin, or a security manager.",
246+ SecurityOp::SetAdvisoryState => "Publish a draft (state published), withdraw a published advisory (state withdrawn), or publish a withdrawn one again. Publishing needs a description, tells webhooks (security_advisory.published) and the repository's watchers who chose security alerts, and makes the advisory visible to anyone who can see the repository. Admin, or a security manager.",
247+ SecurityOp::ListReports => "List the vulnerability reports people sent the repository's maintainers privately, new ones first. Those who see findings (Write, or a security manager). Filter by state (new, accepted, declined). Reporters never see this list.",
248+ SecurityOp::ReportVulnerability => "Report a vulnerability privately to a repository's maintainers: title, description and, if you like, severity. Needs private vulnerability reporting on for the repository. Anyone who can see the repository may report; on a public one you may report without a token by giving an email the maintainers can answer at. At most 5 reports a day per reporter and repository. The workspace's owners and security managers are told at once. You see only your own report.",
249+ SecurityOp::ReviewReport => "Accept a report (decision accept), which makes a draft advisory from it crediting the reporter, or decline it (decision decline), with an optional note. Admin, or a security manager.",
206250 }
207251 }
208252
248292 properties
249293 };
250294 let workspace = || json!({ "type": "string", "description": "The workspace's slug, e.g. \"flagon-io\"." });
295+ let advisory_fields = |mut properties: Value| {
296+ properties["title"] = json!({ "type": "string", "description": "One line: what the vulnerability is. At most 200 characters." });
297+ properties["description"] = json!({ "type": "string", "description": "Markdown: what is affected, how it is exploited, what to do. At most 20,000 characters." });
298+ properties["severity"] = json!({ "type": "string", "enum": ["critical", "high", "medium", "low"], "description": "How bad it is." });
299+ properties["cvss_vector"] = json!({ "type": "string", "description": "A CVSS vector string, when scored: CVSS:3.1/AV:N/…" });
300+ properties["cvss_score"] = json!({ "type": "number", "description": "The CVSS base score, 0 to 10." });
301+ properties["cve_id"] = json!({ "type": "string", "description": "CVE-2026-12345, when one was issued elsewhere." });
302+ properties["ecosystem"] = json!({ "type": "string", "description": "How the affected package is distributed: npm, crates.io, Go, PyPI, or how the project ships." });
303+ properties["package"] = json!({ "type": "string", "description": "The package affected." });
304+ properties["affected_versions"] = json!({ "type": "string", "description": "The versions affected, as a range: >= 1.2.0, < 1.4.1" });
305+ properties["patched_versions"] = json!({ "type": "string", "description": "The versions that fix it: >= 1.4.1" });
306+ properties["credits"] = json!({ "type": "array", "items": { "type": "string" }, "description": "Who found or fixed it: usernames or names, up to 20." });
307+ properties
308+ };
309+ let advisory_id = || json!({ "type": "string", "description": "The advisory's id: adv_…, or its G1TSA-… id." });
251310 let (properties, required): (Value, &[&str]) = match self {
252311 SecurityOp::ListSecretAlerts => (
253312 either(json!({
394453 "review_fail_on": { "type": "string", "enum": ["critical", "high", "medium", "low", "none"], "description": "The lowest severity of a known vulnerability in an added package that fails the review." },
395454 "review_deny_licenses": { "type": "array", "items": { "type": "string" }, "description": "SPDX license ids an added package may not have." },
396455 "review_comment": { "type": "boolean", "description": "Whether the review comments its summary on the pull request." },
456+ "history_alerts": { "type": "boolean", "description": "Whether secrets found in the history, and in pushes too large to scan first, open alerts and tell people. Push protection stays on either way." },
457+ "private_vulnerability_reporting": { "type": "boolean", "description": "Whether anyone who can see the repository may report a vulnerability privately to its maintainers." },
397458 }),
398459 &["repo"],
399460 ),
410471 json!({ "workspace": workspace(), "days": { "type": "integer", "description": "Days of trend, 7 to 90. Default 30." } }),
411472 &["workspace"],
412473 ),
474+ SecurityOp::ListAdvisories => (
475+ json!({ "repo": repo(), "state": { "type": "string", "enum": ["draft", "published", "withdrawn"], "description": "Only advisories in this state." } }),
476+ &["repo"],
477+ ),
478+ SecurityOp::GetAdvisory => (json!({ "repo": repo(), "id": advisory_id() }), &["repo", "id"]),
479+ SecurityOp::CreateAdvisory => (advisory_fields(json!({ "repo": repo() })), &["repo", "title", "severity"]),
480+ SecurityOp::UpdateAdvisory => (advisory_fields(json!({ "repo": repo(), "id": advisory_id() })), &["repo", "id"]),
481+ SecurityOp::SetAdvisoryState => (
482+ json!({
483+ "repo": repo(),
484+ "id": advisory_id(),
485+ "state": { "type": "string", "enum": ["published", "withdrawn"], "description": "published to publish (or publish again), withdrawn to withdraw." },
486+ }),
487+ &["repo", "id", "state"],
488+ ),
489+ SecurityOp::ListReports => (
490+ json!({ "repo": repo(), "state": { "type": "string", "enum": ["new", "accepted", "declined"], "description": "Only reports in this state." } }),
491+ &["repo"],
492+ ),
493+ SecurityOp::ReportVulnerability => (
494+ json!({
495+ "repo": repo(),
496+ "title": { "type": "string", "description": "One line: what you found. At most 200 characters." },
497+ "description": { "type": "string", "description": "What is affected and how to reproduce it. At most 20,000 characters." },
498+ "severity": { "type": "string", "enum": ["critical", "high", "medium", "low"], "description": "How bad you think it is, if you like." },
499+ "email": { "type": "string", "description": "Without a token: where the maintainers can answer you." },
500+ }),
501+ &["repo", "title", "description"],
502+ ),
503+ SecurityOp::ReviewReport => (
504+ json!({
505+ "repo": repo(),
506+ "id": { "type": "string", "description": "The report's id: vrp_…" },
507+ "decision": { "type": "string", "enum": ["accept", "decline"], "description": "accept makes a draft advisory from it; decline closes it." },
508+ "note": { "type": "string", "description": "A note for the record, in a sentence. At most 1,000 characters." },
509+ }),
510+ &["repo", "id", "decision"],
511+ ),
413512 };
414513 let mut schema = json!({ "type": "object", "properties": properties });
415514 if !required.is_empty() {
601700 crate::operations::repo_path(input)
602701 }
603702
703+/// An advisory's fields as given, over `current` for what is not.
704+pub(crate) fn advisory_input(input: &Value, current: &AdvisoryInput) -> AdvisoryInput {
705+ let given = |key: &str, now: &Option<String>| if input.get(key).is_some() { text(input, key) } else { now.clone() };
706+ AdvisoryInput {
707+ title: text(input, "title").unwrap_or_else(|| current.title.clone()),
708+ description: input["description"].as_str().map(str::to_owned).unwrap_or_else(|| current.description.clone()),
709+ severity: text(input, "severity").map(|severity| severity.to_lowercase()).unwrap_or_else(|| current.severity.clone()),
710+ cvss_vector: given("cvss_vector", &current.cvss_vector),
711+ cvss_score: if input.get("cvss_score").is_some() { input["cvss_score"].as_f64() } else { current.cvss_score },
712+ cve_id: given("cve_id", &current.cve_id),
713+ ecosystem: given("ecosystem", &current.ecosystem),
714+ package: given("package", &current.package),
715+ affected_versions: given("affected_versions", &current.affected_versions),
716+ patched_versions: given("patched_versions", &current.patched_versions),
717+ credits: if input["credits"].is_array() { strings(input, "credits") } else { current.credits.clone() },
718+ }
719+}
720+
721+/// An advisory as it stands, as input to change it from.
722+fn advisory_as_input(advisory: &Advisory) -> AdvisoryInput {
723+ AdvisoryInput {
724+ title: advisory.title.clone(),
725+ description: advisory.description.clone(),
726+ severity: advisory.severity.clone(),
727+ cvss_vector: advisory.cvss_vector.clone(),
728+ cvss_score: advisory.cvss_score,
729+ cve_id: advisory.cve_id.clone(),
730+ ecosystem: advisory.ecosystem.clone(),
731+ package: advisory.package.clone(),
732+ affected_versions: advisory.affected_versions.clone(),
733+ patched_versions: advisory.patched_versions.clone(),
734+ credits: advisory.credits.clone(),
735+ }
736+}
737+
604738 pub async fn run(op: SecurityOp, services: &Services, viewer: &Viewer, input: &Value) -> Result<Outcome<Value>> {
605739 let actor = || viewer.clone().unwrap_or_default();
606740 let repo = path_of(input);
9451079 if let Some(on) = flag(input, "review_comment") {
9461080 settings.review_comment = on;
9471081 }
1082+ if let Some(on) = flag(input, "history_alerts") {
1083+ settings.history_alerts = on;
1084+ }
1085+ if let Some(on) = flag(input, "private_vulnerability_reporting") {
1086+ settings.private_vulnerability_reporting = on;
1087+ }
9481088 pass(services, "set_security_settings", &SetSecuritySettingsArgs { actor: actor(), repo, settings }).await
9491089 }
1090+ SecurityOp::ListAdvisories => {
1091+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
1092+ let state = match one_of(input, "state", &["draft", "published", "withdrawn"]) {
1093+ Ok(state) => state,
1094+ Err(message) => return failed(FailureCode::Invalid, &message),
1095+ };
1096+ pass(services, "list_advisories", &ListAdvisoriesArgs { viewer: viewer.clone(), repo, state }).await
1097+ }
1098+ SecurityOp::GetAdvisory => {
1099+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
1100+ pass(services, "get_advisory", &GetAdvisoryArgs { viewer: viewer.clone(), repo, id: text(input, "id").unwrap_or_default() }).await
1101+ }
1102+ SecurityOp::CreateAdvisory => {
1103+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
1104+ let advisory = advisory_input(input, &AdvisoryInput::default());
1105+ pass(services, "create_advisory", &CreateAdvisoryArgs { actor: actor(), repo, advisory }).await
1106+ }
1107+ SecurityOp::UpdateAdvisory => {
1108+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
1109+ let id = text(input, "id").unwrap_or_default();
1110+ // What is not given stays as it is.
1111+ let current: Outcome<Advisory> = call(services, "get_advisory", &GetAdvisoryArgs { viewer: viewer.clone(), repo: repo.clone(), id: id.clone() }).await?;
1112+ let current = match current {
1113+ Outcome::Ok(advisory) => advisory,
1114+ Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
1115+ };
1116+ let advisory = advisory_input(input, &advisory_as_input(&current));
1117+ pass(services, "update_advisory", &UpdateAdvisoryArgs { actor: actor(), repo, id, advisory }).await
1118+ }
1119+ SecurityOp::SetAdvisoryState => {
1120+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
1121+ let state = match one_of(input, "state", &["published", "withdrawn"]) {
1122+ Ok(Some(state)) => state,
1123+ Ok(None) => return failed(FailureCode::Invalid, "state is published or withdrawn."),
1124+ Err(message) => return failed(FailureCode::Invalid, &message),
1125+ };
1126+ pass(services, "set_advisory_state", &SetAdvisoryStateArgs { actor: actor(), repo, id: text(input, "id").unwrap_or_default(), state }).await
1127+ }
1128+ SecurityOp::ListReports => {
1129+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
1130+ let state = match one_of(input, "state", &["new", "accepted", "declined"]) {
1131+ Ok(state) => state,
1132+ Err(message) => return failed(FailureCode::Invalid, &message),
1133+ };
1134+ pass(services, "list_vulnerability_reports", &ListReportsArgs { viewer: viewer.clone(), repo, state }).await
1135+ }
1136+ SecurityOp::ReportVulnerability => {
1137+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
1138+ let args = ReportVulnerabilityArgs {
1139+ viewer: viewer.clone(),
1140+ repo,
1141+ title: text(input, "title").unwrap_or_default(),
1142+ description: text(input, "description").unwrap_or_default(),
1143+ severity: text(input, "severity").map(|severity| severity.to_lowercase()),
1144+ email: text(input, "email"),
1145+ };
1146+ pass(services, "report_vulnerability", &args).await
1147+ }
1148+ SecurityOp::ReviewReport => {
1149+ let Some(repo) = repo else { return failed(FailureCode::Invalid, &need_repo()) };
1150+ let args = ReviewReportArgs {
1151+ actor: actor(),
1152+ repo,
1153+ id: text(input, "id").unwrap_or_default(),
1154+ decision: text(input, "decision").unwrap_or_default().to_lowercase(),
1155+ note: text(input, "note").unwrap_or_default(),
1156+ };
1157+ pass(services, "review_vulnerability_report", &args).await
1158+ }
9501159 SecurityOp::GetWorkspaceSettings => {
9511160 let Some(workspace) = workspace else { return failed(FailureCode::Invalid, "Give the workspace.") };
9521161 pass(services, "workspace_security_settings", &WorkspaceSecuritySettingsArgs { viewer: viewer.clone(), workspace }).await
+9−1
480480 Tool {
481481 name: "security",
482482 title: "Security",
483− description: "A repository's security: secret scanning alerts and push protection bypasses, custom secret patterns, code scanning alerts and SARIF uploads, vulnerability alerts, the dependency graph and its SBOM, dependency review, settings, and a workspace's overview. Fix an alert with g1t. Findings are shown to those who can change the code only. Give `repo` (owner/name), or `workspace` for lists across one.",
483+ description: "A repository's security: secret scanning alerts and push protection bypasses, custom secret patterns, code scanning alerts and SARIF uploads, vulnerability alerts, the dependency graph and its SBOM, dependency review, settings, a workspace's overview, security advisories and private vulnerability reports. Fix an alert with g1t. Findings are shown to those who can change the code only; published advisories to anyone who can see the repository. Give `repo` (owner/name), or `workspace` for lists across one.",
484484 default_action: Some("secret_alerts"),
485485 actions: &[
486486 a("secret_alerts", Op::Security(SecurityOp::ListSecretAlerts), "Secret scanning alerts; by state, secret_type, validity, bypassed"),
514514 a("workspace_settings", Op::Security(SecurityOp::GetWorkspaceSettings), "A workspace's delegated bypass and validity checks"),
515515 a("update_workspace_settings", Op::Security(SecurityOp::UpdateWorkspaceSettings), "Turn delegated bypass or validity checks on or off"),
516516 a("overview", Op::Security(SecurityOp::GetOverview), "A workspace's alerts, trends and coverage"),
517+ a("advisories", Op::Security(SecurityOp::ListAdvisories), "A repository's security advisories; by state"),
518+ a("advisory", Op::Security(SecurityOp::GetAdvisory), "One advisory by adv_… or G1TSA-… id"),
519+ a("create_advisory", Op::Security(SecurityOp::CreateAdvisory), "Draft a security advisory"),
520+ a("update_advisory", Op::Security(SecurityOp::UpdateAdvisory), "Change an advisory's fields"),
521+ a("set_advisory_state", Op::Security(SecurityOp::SetAdvisoryState), "Publish or withdraw an advisory"),
522+ a("reports", Op::Security(SecurityOp::ListReports), "Vulnerability reports sent to the maintainers; by state"),
523+ a("report_vulnerability", Op::Security(SecurityOp::ReportVulnerability), "Report a vulnerability privately to a repository's maintainers"),
524+ a("review_report", Op::Security(SecurityOp::ReviewReport), "Accept a report as a draft advisory, or decline it"),
517525 ],
518526 },
519527 Tool {
+0−0

Binary or large file; its contents are not shown.