Skip to content
190 linesCodeBlameRaw

Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.

Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1//! What the security suite's parts share: whether a repository has the
2//! paid features, telling people (events for webhooks and the inbox), and
3//! the audit log.
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.4//!
5//! Who hears what: per-alert events (`<type>.created`, `.fixed`, …) go to
6//! webhooks. The inbox reads the one summary each scan publishes
7//! (summaries.rs), bypass requests and their answers, and vulnerability
8//! reports; each names the workspace's owners and security managers as
9//! `notify`, and its members as `members`, those who may see findings.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar10
11use g1t_contracts::audit::{AuditActor, AuditOutcome, AuditTarget, NewAuditEntry, RecordAuditArgs, Surface};
12use g1t_contracts::billing::{Feature, HasFeatureArgs};
13use g1t_contracts::events::{NewEvent, Publish};
14use g1t_contracts::identity::{ListMembersArgs, Member};
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.15use g1t_contracts::advisories::AdvisoryEvent;
16use g1t_contracts::security_suite::{AlertType, EVENT_TYPES, PaidFeature, SecurityEvent, SecuritySummary, needs_activation};
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar17use g1t_contracts::{FailureCode, Outcome, Role, User, new_id};
18use g1t_kit::now_ms;
19use worker::Result;
20
21use crate::Security;
22use crate::store::RepoRow;
23
24/// The most workspace members an event names as able to see findings.
25const MAX_MEMBERS_NAMED: usize = 500;
26
27/// A failure for want of the activation.
28pub fn payment_required<T>(feature: PaidFeature, workspace: &str) -> Outcome<T> {
29 Outcome::fail(FailureCode::PaymentRequired, needs_activation(feature, workspace))
30}
31
32/// The page of a repository's security section: `/acme/rocket/security/…`.
33pub fn link(repo: &RepoRow, rest: &str) -> String {
34 let rest = rest.trim_start_matches('/');
35 if rest.is_empty() {
36 format!("/{}/{}/security", repo.namespace, repo.name)
37 } else {
38 format!("/{}/{}/security/{rest}", repo.namespace, repo.name)
39 }
40}
41
42impl Security {
43 /// Whether the workspace has the Security and quality activation (or
44 /// has it included). When billing cannot say, it is taken as not: a
45 /// paid feature waits rather than running unpaid.
46 pub(crate) async fn activated(&self, namespace: &str) -> bool {
47 let answer: Result<Outcome<bool>> = g1t_kit::call(
48 &self.billing,
49 "has_feature",
50 &HasFeatureArgs { workspace: namespace.to_owned(), feature: Feature::Security },
51 )
52 .await;
53 match answer {
54 Ok(Outcome::Ok(on)) => on,
55 Ok(Outcome::Fail(_)) => false,
56 Err(error) => {
57 worker::console_error!("security: has_feature for {namespace}: {error}");
58 false
59 }
60 }
61 }
62
63 /// Whether a repository has the paid features: it is public, or its
64 /// workspace has the activation.
65 pub(crate) async fn entitled(&self, repo: &RepoRow) -> Result<bool> {
66 let (_, private) = self.store.repo_settings(&repo.repo_id).await?;
67 Ok(!private || self.activated(&repo.namespace).await)
68 }
69
70 /// `None` when the repository may use `feature`, or the refusal.
71 pub(crate) async fn gate<T>(&self, repo: &RepoRow, feature: PaidFeature) -> Result<Option<Outcome<T>>> {
72 Ok((!self.entitled(repo).await?).then(|| payment_required(feature, &repo.namespace)))
73 }
74
75 /// The workspace's members, as g1t sees them.
76 pub(crate) async fn members(&self, namespace: &str) -> Vec<Member> {
77 let found: Result<Outcome<Vec<Member>>> = g1t_kit::call(
78 &self.identity,
79 "list_members",
80 &ListMembersArgs { slug: namespace.to_owned(), viewer: Some(User::system(namespace)) },
81 )
82 .await;
83 match found {
84 Ok(Outcome::Ok(members)) => members,
85 Ok(Outcome::Fail(failure)) => {
86 worker::console_error!("security: members of {namespace}: {}", failure.message);
87 Vec::new()
88 }
89 Err(error) => {
90 worker::console_error!("security: members of {namespace}: {error}");
91 Vec::new()
92 }
93 }
94 }
95
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.96 /// Publishes a security event about one alert, for webhooks (and, for
97 /// a bypass request, the inbox). What the inbox tells people of new
98 /// alerts comes from the scan's summary (summaries.rs), not from these.
99 /// Failing to publish never fails the change.
100 pub(crate) async fn publish(&self, kind: &str, repo: &RepoRow, event: SecurityEvent, actor_id: Option<String>) {
101 let data = serde_json::to_value(&event).unwrap_or_default();
102 let audience = kind == "secret_scanning.bypass_requested" && event.notify.is_empty();
103 self.publish_data(kind, repo, data, audience, actor_id).await;
104 }
105
106 /// Publishes a scan's summary (`<type>.summary`): the inbox's one item
107 /// per repository and alert type, told to the workspace's owners and
108 /// security managers, and to watchers who chose security and may see
109 /// findings.
110 pub(crate) async fn publish_summary(&self, kind: &str, repo: &RepoRow, summary: &SecuritySummary, actor_id: Option<String>) {
111 let data = serde_json::to_value(summary).unwrap_or_default();
112 self.publish_data(kind, repo, data, true, actor_id).await;
113 }
114
115 /// Publishes an advisory event (`security_advisory.*`). A report tells
116 /// the owners and security managers; a published advisory, webhooks
117 /// and the repository's security watchers.
118 pub(crate) async fn publish_advisory(&self, kind: &str, repo: &RepoRow, event: &AdvisoryEvent, actor_id: Option<String>) {
119 let data = serde_json::to_value(event).unwrap_or_default();
120 self.publish_data(kind, repo, data, kind == "security_advisory.reported" || kind == "security_advisory.published", actor_id).await;
121 }
122
123 /// Publishes one of [`EVENT_TYPES`]. With `audience`, `notify` (when
124 /// empty) becomes the workspace's owners and security managers, and
125 /// `members` names its members, as those who may see findings.
126 async fn publish_data(&self, kind: &str, repo: &RepoRow, mut data: serde_json::Value, audience: bool, actor_id: Option<String>) {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar127 let Some(kind) = EVENT_TYPES.iter().copied().find(|known| *known == kind) else {
128 worker::console_error!("security: no event called {kind}");
129 return;
130 };
131 let Some(events) = &self.events else { return };
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.132 if audience {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar133 let members = self.members(&repo.namespace).await;
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.134 if data["notify"].as_array().is_none_or(Vec::is_empty) {
135 data["notify"] = serde_json::json!(
136 members
137 .iter()
138 .filter(|member| member.role == Role::Owner || member.org_roles.contains(&g1t_contracts::OrgRole::SecurityManager))
139 .map(|member| member.username.clone())
140 .collect::<Vec<_>>()
141 );
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar142 }
143 data["members"] = serde_json::json!(
144 members.iter().take(MAX_MEMBERS_NAMED).map(|member| member.username.clone()).collect::<Vec<_>>()
145 );
146 }
147 let published: Result<serde_json::Value> = g1t_kit::call(
148 events,
149 "publish",
150 &Publish {
151 events: vec![NewEvent { kind, source: "security", repo_id: Some(repo.repo_id.clone()), actor: actor_id, data }],
152 },
153 )
154 .await;
155 if let Err(error) = published {
156 worker::console_error!("security: {kind} for {} not published: {error}", repo.repo_id);
157 }
158 }
159
160 /// Publishes `<type>.<action>` for one alert.
161 pub(crate) async fn alert_event(&self, alert_type: AlertType, action: &str, repo: &RepoRow, event: SecurityEvent, actor_id: Option<String>) {
162 let kind = format!("{}.{action}", alert_type.event_prefix());
163 self.publish(&kind, repo, event, actor_id).await;
164 }
165
166 /// Records a security decision in the workspace's audit log.
167 pub(crate) async fn audit(&self, actor: &User, action: &str, repo: Option<&RepoRow>, namespace: &str, path: Option<&str>, message: &str) {
168 let Some(events) = &self.events else { return };
169 let entry = NewAuditEntry {
170 actor: AuditActor::of(actor),
171 action: action.to_owned(),
172 surface: Surface::Web,
173 target: AuditTarget {
174 workspace: namespace.to_owned(),
175 repo: repo.map(|repo| format!("{}/{}", repo.namespace, repo.name)),
176 path: path.map(str::to_owned),
177 ..AuditTarget::default()
178 },
179 outcome: AuditOutcome::Allowed,
180 rule: "security".to_owned(),
181 result: Some("ok".to_owned()),
182 message: Some(message.chars().take(500).collect()),
183 request_id: new_id("req", now_ms()),
184 };
185 let recorded: Result<u32> = g1t_kit::call(events, "audit_record", &RecordAuditArgs { entries: vec![entry] }).await;
186 if let Err(error) = recorded {
187 worker::console_error!("security: audit entry {action} not recorded: {error}");
188 }
189 }
190}