Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
866 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.

status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1//! The security service: what g1t finds wrong in a repository, and the
2//! upkeep that fixes it without a person.
3//!
4//! - Secrets. The repos service refuses pushes that add one
5//! (`push_blocked` says which were allowed and records the rest), and
6//! each repository's history is scanned once, in the background.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily7//! A push too large to scan first is let through, and its new commits
8//! are scanned after they land (`history::advance_push_scan`).
9//! - Alerts are open, dismissed (with a reason, a comment and who) or
10//! fixed, and each keeps an activity log. Likely test values are listed
11//! apart and never block a push or count as critical.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas12//! - Dependencies. On every push to a default branch, and daily, the
13//! lockfiles are read and every package checked against OSV. Each
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily14//! vulnerable package with a fix gets a security update: g1t itself
15//! (`User::system`) opens a pull request raising its version, made in a
16//! sandbox (the runner's `bump`), which lands through the branch's
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent17//! required checks. Only when code has to change is g1t put on an
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily18//! issue for it.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas19//!
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar20//! - The security suite (`g1t_contracts::security_suite`): custom secret
21//! patterns (`patterns`), push protection bypasses, their review and
22//! validity checks (`secret_alerts`), code scanning from SARIF uploads
23//! and its pull request check (`code_scanning`), the dependency graph,
24//! its SBOM and dependency review (`supply_chain`), "Fix with g1t"
25//! (`fixes`), and settings with the workspace's overview (`overview`).
26//! On private repositories its paid parts need the workspace's Security
27//! and quality activation (`suite`); it tells people through events.
28//!
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas29//! Other services reach it over `POST /rpc/<method>`; see
30//! `g1t_contracts::security`.
31
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r932mod advisories;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar33mod code_scanning;
34mod config;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas35mod deps;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar36mod fixes;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas37mod history;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar38mod manifests;
39mod overview;
40mod patterns;
41mod planning;
42mod pull_text;
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches43mod resolved;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar44mod ranges;
45mod registries;
46mod schedule;
47mod secret_alerts;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily48mod security_updates;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas49mod store;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar50mod suite;
51mod suite_store;
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r952mod summaries;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar53mod supply_chain;
54mod timezones;
55mod update_store;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily56mod updates;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar57mod version_updates;
58mod yaml;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas59
60use g1t_contracts::access::{self, Capability};
61use g1t_contracts::events::{Event, WorkspaceRenamed};
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily62use g1t_contracts::security::UPDATE_BRANCH_PREFIX;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas63use g1t_contracts::repos::{GetArgs, PathByIdArgs, Repo, RepoPath, RepoStatus, StatusByIdArgs};
64use g1t_contracts::security::*;
65use g1t_contracts::time::rfc3339;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily66use g1t_contracts::{FailureCode, Outcome, User};
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas67use g1t_kit::{args, now_ms, reply, rpc_method};
68use serde::Deserialize;
69use worker::{Context, Env, Fetcher, MessageBatch, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
70
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily71use store::{Activity, RepoRow, Store};
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas72
73/// Repositories whose history is continued per sweep, and pages each.
74const HISTORIES_PER_SWEEP: u32 = 5;
75const PAGES_PER_SWEEP: u32 = 4;
76/// Repositories whose dependencies are read again per sweep.
77const DEPENDENCIES_PER_SWEEP: u32 = 10;
78const DAY_MS: u64 = 24 * 60 * 60 * 1000;
79const MAX_REASON_CHARS: usize = 500;
Merge main (membership, two-factor, GitHub repo roles) into tokens80/// What seeing and dismissing a repository's findings takes: the Write
81/// role, as on GitHub, or a security manager of its workspace. Changing its
82/// security settings takes Admin (`ManageSecurity`).
83const SEE_FINDINGS: Capability = Capability::SecurityAlerts;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas84
85pub struct Security {
86 store: Store,
87 identity: Fetcher,
88 repos: Fetcher,
89 work: Fetcher,
90 runner: Fetcher,
91 billing: Fetcher,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar92 actions: Fetcher,
93 /// The events service: security events for webhooks and the inbox,
94 /// and audit entries. Optional so a deployment without the binding
95 /// still scans.
96 events: Option<Fetcher>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas97}
98
99fn fail<T>(code: FailureCode, message: impl Into<String>) -> Outcome<T> {
100 Outcome::fail(code, message)
101}
102
103/// `git.push`, as far as this service reads it.
104#[derive(Deserialize)]
105#[serde(rename_all = "camelCase")]
106struct Pushed {
107 repo_id: String,
108 #[serde(default)]
109 default_branch: bool,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily110 #[serde(default, rename = "ref")]
111 git_ref: String,
112 #[serde(default)]
113 before: Option<String>,
114 #[serde(default)]
115 after: String,
116 /// Too large to scan before it was stored.
117 #[serde(default)]
118 unscanned: bool,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas119}
120
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily121/// `pull.merged`, `pull.closed` and `checks.completed`, as far as this
122/// service reads them.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas123#[derive(Deserialize)]
124#[serde(rename_all = "camelCase")]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily125struct PullHappened {
126 repo_id: String,
127 number: u32,
128 #[serde(default)]
129 status: Option<String>,
130}
131
132/// The longest comment a dismissal keeps.
133const MAX_COMMENT_CHARS: usize = MAX_REASON_CHARS;
134/// Activity rows the Security page reads, newest first.
135const ACTIVITY_SHOWN: u32 = 500;
136
137#[derive(Deserialize)]
138#[serde(rename_all = "camelCase")]
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas139struct Created {
140 repo_id: String,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit141 /// Absent from events older than the field.
142 #[serde(default)]
143 is_private: Option<bool>,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas144}
145
146impl Security {
147 fn new(env: &Env) -> Result<Self> {
148 Ok(Security {
149 store: Store { db: env.d1("DB")? },
150 identity: env.service("IDENTITY")?,
151 repos: env.service("REPOS")?,
152 work: env.service("WORK")?,
153 runner: env.service("RUNNER")?,
154 billing: env.service("BILLING")?,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar155 actions: env.service("ACTIONS")?,
156 events: env.service("EVENTS").ok(),
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas157 })
158 }
159
160 /// The repository at `path`, recorded here, if `viewer` may see its
161 /// findings (the Write role) and do `capability`. Findings are for those
162 /// who can change the code: to anyone else the page does not exist,
163 /// public repository or not.
164 async fn member_repo(
165 &self,
166 path: &RepoPath,
167 viewer: &Option<User>,
168 capability: Capability,
169 ) -> Result<Outcome<RepoRow>> {
170 let hidden = || fail(FailureCode::NotFound, "Repository not found.");
171 if viewer.is_none() {
172 return Ok(hidden());
173 }
174 let repo: Outcome<Repo> =
175 g1t_kit::call(&self.repos, "get", &GetArgs { path: path.clone(), viewer: viewer.clone() }).await?;
176 let repo = match repo {
177 Outcome::Ok(repo) if repo.fork_of.is_none() => repo,
178 _ => return Ok(hidden()),
179 };
180 if !access::can(viewer.as_ref(), &repo, SEE_FINDINGS) {
181 return Ok(hidden());
182 }
183 if !access::can(viewer.as_ref(), &repo, capability) {
184 return Ok(fail(
185 FailureCode::Forbidden,
186 access::needs(capability, &format!("{}/{}", repo.namespace, repo.name)),
187 ));
188 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar189 let row = self.store.register(&repo.id, &repo.namespace, &repo.name).await?;
190 self.store.set_private(&repo.id, repo.is_private).await?;
191 Ok(Outcome::Ok(row))
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas192 }
193
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.194 /// Where the repository stands: archived, deleted, a mirror. When repos
195 /// cannot say, it is taken as an ordinary active repository.
196 async fn status(&self, repo_id: &str) -> Result<RepoStatus> {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas197 let status: Result<RepoStatus> =
198 g1t_kit::call(&self.repos, "status_by_id", &StatusByIdArgs { id: repo_id.to_owned() }).await;
199 Ok(match status {
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.200 Ok(status) => status,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas201 Err(error) => {
202 worker::console_error!("security: status_by_id {repo_id}: {error}");
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.203 RepoStatus::default()
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas204 }
205 })
206 }
207
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.208 /// Whether the repository is neither archived nor deleted. Scanning
209 /// goes on in a mirror: it reads.
210 async fn active(&self, repo_id: &str) -> Result<bool> {
211 Ok(self.status(repo_id).await?.active())
212 }
213
214 /// Whether g1t may open branches and pull requests here on its own
215 /// (security and dependency updates): active, and not a mirror short
216 /// of a takeover (`g1t_contracts::mirrors`). `Err` says why not.
217 async fn takes_updates(&self, repo: &RepoRow) -> Result<std::result::Result<(), String>> {
218 let status = self.status(&repo.repo_id).await?;
219 if status.takes_updates() {
220 return Ok(Ok(()));
221 }
222 Ok(Err(match &status.mirror {
223 Some(mirror) if status.active() => g1t_contracts::mirrors::updates_wait_message(&repo.namespace, &repo.name, mirror),
224 _ => "Updates wait while the repository is archived or deleted.".to_owned(),
225 }))
226 }
227
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas228 /// Records a repository named in an event, by id. Forks are not
229 /// recorded: a pull request's findings belong to its repository.
230 async fn register_by_id(&self, repo_id: &str) -> Result<Option<RepoRow>> {
231 if let Some(row) = self.store.repo(repo_id).await? {
232 return Ok(Some(row));
233 }
234 let path: Option<RepoPath> = g1t_kit::call(&self.repos, "path_by_id", &PathByIdArgs { id: repo_id.to_owned() }).await?;
235 match path {
236 Some(path) => Ok(Some(self.store.register(repo_id, &path.namespace, &path.name).await?)),
237 None => Ok(None),
238 }
239 }
240
241 async fn overview(&self, a: OverviewArgs) -> Result<Outcome<SecurityOverview>> {
242 let mut repo = match self.member_repo(&a.repo, &a.viewer, SEE_FINDINGS).await? {
243 Outcome::Ok(repo) => repo,
244 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
245 };
246 // The first look at a repository reads its dependencies at once;
247 // its history is scanned in the background.
248 if repo.deps_scanned_at.is_none() {
249 self.scan_dependencies(&repo).await?;
250 repo = self.store.repo(&repo.repo_id).await?.unwrap_or(repo);
251 }
252 let (counts, _, _) = self.store.counts(&repo.repo_id).await?;
253 Ok(Outcome::Ok(SecurityOverview {
254 repo_id: repo.repo_id.clone(),
255 counts,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily256 secret_counts: self.store.secret_counts(&repo.repo_id).await?,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas257 secrets: self.store.secrets(&repo.repo_id).await?,
258 vulnerabilities: self.store.vulnerabilities(&repo.repo_id).await?,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily259 activity: self.store.activity(&repo.repo_id, ACTIVITY_SHOWN).await?,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas260 scan: repo.scan_state(),
261 upkeep: repo.upkeep != 0,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar262 version_updates: self.version_updates_view(&repo).await?,
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas263 }))
264 }
265
Merge main (membership, two-factor, GitHub repo roles) into tokens266 /// What dismissing or reopening alert `id` takes: Write, for a secret
267 /// as for a vulnerable dependency, as on GitHub; a security manager may
268 /// too.
269 fn capability_for(_id: &str) -> Capability {
270 SEE_FINDINGS
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily271 }
272
273 async fn dismiss(&self, a: DismissArgs) -> Result<Outcome<AlertChange>> {
274 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Self::capability_for(&a.id)).await? {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas275 Outcome::Ok(repo) => repo,
276 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
277 };
278 if !a.actor.verified {
279 return Ok(fail(FailureCode::Forbidden, "Confirm your email address first."));
280 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily281 let comment: String = a.comment.trim().chars().take(MAX_COMMENT_CHARS).collect();
282 let comment = (!comment.is_empty()).then_some(comment);
283 if let Some(finding) = self.store.secret(&repo.repo_id, &a.id).await? {
284 if !a.reason.for_secrets() {
285 return Ok(fail(
286 FailureCode::Invalid,
287 "A secret is dismissed as false_positive, used_in_tests, revoked or wont_fix.",
288 ));
289 }
290 if finding.state != AlertState::Open {
291 return Ok(fail(FailureCode::Conflict, "This alert is not open. Reopen it first to dismiss it again."));
292 }
293 self.store
294 .dismiss_secret(&repo.repo_id, &a.id, a.reason, &a.actor.username, comment.as_deref())
295 .await?;
296 self.store
297 .record(&repo.repo_id, &[Activity {
298 alert_id: &a.id,
299 action: "dismissed",
300 actor: Some(&a.actor.username),
301 reason: Some(a.reason),
302 comment: comment.as_deref(),
303 number: None,
304 }])
305 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar306 let secret = self.store.secret(&repo.repo_id, &a.id).await?;
307 if let Some(secret) = &secret {
308 // Revoked is fixed; any other reason, dismissed.
309 let action = if a.reason == DismissReason::Revoked { "fixed" } else { "dismissed" };
310 let event = g1t_contracts::security_suite::SecurityEvent {
311 reason: Some(a.reason.as_str().to_owned()),
312 ..secret_alerts::secret_event(&repo, secret)
313 };
314 self.alert_event(g1t_contracts::security_suite::AlertType::SecretScanning, action, &repo, event, Some(a.actor.id.clone()))
315 .await;
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9316 let changed = summaries::Changed {
317 scan: "change",
318 fixed: if action == "fixed" { vec![a.id.clone()] } else { Vec::new() },
319 dismissed: u32::from(action == "dismissed"),
320 ..summaries::Changed::default()
321 };
322 self.summarize(&repo, g1t_contracts::security_suite::AlertType::SecretScanning, changed, Some(a.actor.id.clone())).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar323 }
324 return Ok(Outcome::Ok(AlertChange { secret, vulnerability: None }));
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily325 }
326 let Some(vuln) = self.store.vulnerability(&repo.repo_id, &a.id).await? else {
327 return Ok(fail(FailureCode::NotFound, "No such alert."));
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas328 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily329 if a.reason.for_secrets() {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas330 return Ok(fail(
331 FailureCode::Invalid,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily332 "A dependency is dismissed as fix_started, no_bandwidth, tolerable_risk, inaccurate or not_used.",
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas333 ));
334 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily335 if vuln.state != AlertState::Open {
336 return Ok(fail(FailureCode::Conflict, "This alert is not open. Reopen it first to dismiss it again."));
337 }
338 self.store
339 .dismiss_vulnerability(&repo.repo_id, &a.id, a.reason, &a.actor.username, comment.as_deref())
340 .await?;
341 self.store
342 .record(&repo.repo_id, &[Activity {
343 alert_id: &a.id,
344 action: "dismissed",
345 actor: Some(&a.actor.username),
346 reason: Some(a.reason),
347 comment: comment.as_deref(),
348 number: None,
349 }])
350 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar351 let vulnerability = self.store.vulnerability(&repo.repo_id, &a.id).await?;
352 if let Some(vuln) = &vulnerability {
353 let event = g1t_contracts::security_suite::SecurityEvent { reason: Some(a.reason.as_str().to_owned()), ..deps::vulnerability_event(&repo, vuln) };
354 self.alert_event(g1t_contracts::security_suite::AlertType::Vulnerability, "dismissed", &repo, event, Some(a.actor.id.clone())).await;
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9355 let changed = summaries::Changed { scan: "change", dismissed: 1, ..summaries::Changed::default() };
356 self.summarize(&repo, g1t_contracts::security_suite::AlertType::Vulnerability, changed, Some(a.actor.id.clone())).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar357 }
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches358 // A security update whose alerts are now all fixed or dismissed closes.
359 if let Err(error) = self.resolve_updates(&repo, None).await {
360 worker::console_error!("security: updates of {} not resolved after a dismissal: {error}", repo.repo_id);
361 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar362 Ok(Outcome::Ok(AlertChange { secret: None, vulnerability }))
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily363 }
364
365 async fn reopen(&self, a: ReopenArgs) -> Result<Outcome<AlertChange>> {
366 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Self::capability_for(&a.id)).await? {
367 Outcome::Ok(repo) => repo,
368 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas369 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily370 if !a.actor.verified {
371 return Ok(fail(FailureCode::Forbidden, "Confirm your email address first."));
372 }
373 let reopened = Activity { alert_id: &a.id, action: "reopened", actor: Some(&a.actor.username), reason: None, comment: None, number: None };
374 if let Some(finding) = self.store.secret(&repo.repo_id, &a.id).await? {
375 if finding.state == AlertState::Open {
376 return Ok(fail(FailureCode::Conflict, "This alert is already open."));
377 }
378 // A secret that never landed has nothing to reopen to but blocked.
379 let status = if finding.source == "push" && finding.test_value.is_none() { SecretStatus::Blocked } else { SecretStatus::Open };
380 self.store.reopen_secret(&repo.repo_id, &a.id, status).await?;
381 self.store.record(&repo.repo_id, &[reopened]).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar382 let secret = self.store.secret(&repo.repo_id, &a.id).await?;
383 if let Some(secret) = &secret {
384 self.alert_event(
385 g1t_contracts::security_suite::AlertType::SecretScanning,
386 "reopened",
387 &repo,
388 secret_alerts::secret_event(&repo, secret),
389 Some(a.actor.id.clone()),
390 )
391 .await;
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9392 // The thread's counts move; nobody is told anew of what they reopened.
393 let changed = summaries::Changed { scan: "change", ..summaries::Changed::default() };
394 self.summarize(&repo, g1t_contracts::security_suite::AlertType::SecretScanning, changed, Some(a.actor.id.clone())).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar395 }
396 return Ok(Outcome::Ok(AlertChange { secret, vulnerability: None }));
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily397 }
398 let Some(vuln) = self.store.vulnerability(&repo.repo_id, &a.id).await? else {
399 return Ok(fail(FailureCode::NotFound, "No such alert."));
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas400 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily401 if vuln.state != AlertState::Dismissed {
402 return Ok(fail(FailureCode::Conflict, "Only a dismissed alert can be reopened; a fixed one reopens when it is found again."));
403 }
404 self.store.reopen_vulnerability(&repo.repo_id, &a.id).await?;
405 self.store.record(&repo.repo_id, &[reopened]).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar406 let vulnerability = self.store.vulnerability(&repo.repo_id, &a.id).await?;
407 if let Some(vuln) = &vulnerability {
408 self.alert_event(
409 g1t_contracts::security_suite::AlertType::Vulnerability,
410 "reopened",
411 &repo,
412 deps::vulnerability_event(&repo, vuln),
413 Some(a.actor.id.clone()),
414 )
415 .await;
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9416 let changed = summaries::Changed { scan: "change", ..summaries::Changed::default() };
417 self.summarize(&repo, g1t_contracts::security_suite::AlertType::Vulnerability, changed, Some(a.actor.id.clone())).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar418 }
419 Ok(Outcome::Ok(AlertChange { secret: None, vulnerability }))
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas420 }
421
422 async fn rescan(&self, a: RescanArgs) -> Result<Outcome<ScanState>> {
423 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), SEE_FINDINGS).await? {
424 Outcome::Ok(repo) => repo,
425 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
426 };
427 self.store.restart_history(&repo.repo_id).await?;
428 self.scan_dependencies(&repo).await?;
429 if let Some(fresh) = self.store.repo(&repo.repo_id).await? {
430 self.advance_history(&fresh, 1).await?;
431 }
432 let state = self.store.repo(&repo.repo_id).await?.map(|row| row.scan_state()).unwrap_or_default();
433 Ok(Outcome::Ok(state))
434 }
435
436 async fn set_upkeep(&self, a: SetUpkeepArgs) -> Result<Outcome<bool>> {
437 // Whether agents keep its dependencies up to date is one of its
438 // settings.
439 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Capability::ManageSettings).await? {
440 Outcome::Ok(repo) => repo,
441 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
442 };
443 if !a.actor.verified {
444 return Ok(fail(FailureCode::Forbidden, "Confirm your email address first."));
445 }
446 self.store.set_upkeep(&repo.repo_id, a.enabled, &a.actor.username).await?;
447 Ok(Outcome::Ok(a.enabled))
448 }
449
450 async fn workspace(&self, a: WorkspaceArgs) -> Result<Outcome<Vec<RepoSecurity>>> {
451 let workspace = a.workspace.to_lowercase();
452 if !a.viewer.as_ref().is_some_and(|user| user.is_member(&workspace)) {
453 return Ok(fail(FailureCode::NotFound, "Workspace not found."));
454 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit455 // Only the repositories whose findings the viewer may see. The
456 // Write role is never had through being public, so treating each
457 // as private changes nothing.
458 let repos: Vec<RepoRow> = self
459 .store
460 .in_namespace(&workspace)
461 .await?
462 .into_iter()
463 .filter(|repo| {
464 let target = access::RepoRef { id: &repo.repo_id, namespace: &workspace, private: true };
465 access::can(a.viewer.as_ref(), target, SEE_FINDINGS)
466 })
467 .collect();
468 // Every repository's counts at once, not one after another.
469 let counts = futures_util::future::try_join_all(repos.iter().map(|repo| self.store.counts(&repo.repo_id))).await?;
470 let list = repos
471 .into_iter()
472 .zip(counts)
473 .map(|(repo, (counts, secrets, vulnerabilities))| RepoSecurity {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas474 repo_id: repo.repo_id,
475 name: repo.name,
476 counts,
477 secrets,
478 vulnerabilities,
479 upkeep: repo.upkeep != 0,
480 dependencies_scanned_at: repo.deps_scanned_at,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit481 })
482 .collect();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas483 Ok(Outcome::Ok(list))
484 }
485
486 /// Push protection's question: which of these secrets were allowed?
487 /// The others are recorded as blocked, so someone can allow them.
488 async fn push_blocked(&self, a: PushBlockedArgs) -> Result<PushVerdict> {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar489 let repo = self.store.register(&a.repo_id, &a.path.namespace, &a.path.name).await?;
490 if let Some(private) = a.private {
491 self.store.set_private(&a.repo_id, private).await?;
492 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas493 let fingerprints: Vec<String> = a.secrets.iter().map(|secret| secret.fingerprint.clone()).collect();
494 let known = self.store.known(&a.repo_id, &fingerprints).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar495 let mut allowed = let_through(&known, &a.secrets);
496 // Bypassed with a reason: let through, whatever the alert says now.
497 allowed.extend(self.store.bypassed(&a.repo_id, &fingerprints).await?);
498 allowed.sort();
499 allowed.dedup();
500 let all = a.secrets.clone();
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas501 let fresh: Vec<NewSecret> = a
502 .secrets
503 .into_iter()
504 .filter(|secret| !known.iter().any(|(fingerprint, _, _)| *fingerprint == secret.fingerprint))
505 .collect();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily506 // A likely test value goes through, so it lands: open, not blocked.
507 let (tests, real): (Vec<NewSecret>, Vec<NewSecret>) = fresh.into_iter().partition(|secret| secret.test_value.is_some());
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas508 self.store
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily509 .add_secrets(&a.repo_id, &real, SecretStatus::Blocked, "push", a.pusher.as_deref())
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas510 .await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily511 self.store
512 .add_secrets(&a.repo_id, &tests, SecretStatus::Open, "push", a.pusher.as_deref())
513 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar514 let fresh: Vec<String> = real.iter().map(|secret| secret.fingerprint.clone()).collect();
515 self.secrets_found(&repo, &all, "push", &fresh, a.pusher.as_deref()).await?;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas516 let ids = self
517 .store
518 .known(&a.repo_id, &fingerprints)
519 .await?
520 .into_iter()
521 .filter(|(fingerprint, _, _)| !allowed.contains(fingerprint))
522 .map(|(fingerprint, id, _)| (fingerprint, id))
523 .collect();
524 Ok(PushVerdict { allowed, ids })
525 }
526
527 /// What happens on the bus that concerns this service.
528 async fn on_event(&self, event: &Event) -> Result<()> {
529 match event.kind.as_str() {
530 "git.push" => {
531 let Ok(pushed) = serde_json::from_value::<Pushed>(event.data.clone()) else {
532 return Ok(());
533 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily534 // Too large to scan before it was stored: its new commits
535 // are scanned now, on whichever branch.
536 if pushed.unscanned
537 && !pushed.after.is_empty()
538 && let Some(repo) = self.register_by_id(&pushed.repo_id).await?
539 {
540 let id = self
541 .store
542 .add_push_scan(&repo.repo_id, &pushed.git_ref, &pushed.after, pushed.before.as_deref(), event.actor.as_deref())
543 .await?;
544 self.advance_push_scan(&id, history::PUSH_PAGES_AT_ONCE).await?;
545 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar546 // A version update's branch, or a grouped security update's,
547 // pushed by its sandbox: time for its pull request.
548 if !pushed.default_branch
549 && let Some(branch) = pushed.git_ref.strip_prefix("refs/heads/")
550 && self.update_pull_pushed(&pushed.repo_id, branch, &pushed.after).await?
551 {
552 return Ok(());
553 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily554 // A security update's branch, pushed by its sandbox: time for
555 // its pull request.
556 if let Some(branch) = pushed.git_ref.strip_prefix("refs/heads/")
557 && branch.starts_with(UPDATE_BRANCH_PREFIX)
558 {
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches559 self.update_pushed(&pushed.repo_id, branch, &pushed.after).await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily560 return Ok(());
561 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas562 if !pushed.default_branch {
563 return Ok(());
564 }
565 if let Some(repo) = self.register_by_id(&pushed.repo_id).await? {
566 self.scan_dependencies(&repo).await?;
567 if repo.history != "done" {
568 self.advance_history(&repo, 1).await?;
569 }
570 }
571 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily572 "pull.merged" | "pull.closed" | "checks.completed" => {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar573 if let Ok(happened) = serde_json::from_value::<PullHappened>(event.data.clone())
574 && !(event.kind != "checks.completed" && self.update_pull_closed(&event.kind, &happened.repo_id, happened.number).await?)
575 {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily576 self.update_pull_event(
577 &event.kind,
578 &happened.repo_id,
579 happened.number,
580 happened.status.as_deref(),
581 event.actor.as_deref(),
582 )
583 .await?;
584 }
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches585 // One of g1t's update pull requests closed or merged, by g1t
586 // or by a person: its branch goes.
587 if event.kind != "checks.completed"
588 && let Ok(happened) = serde_json::from_value::<PullHappened>(event.data.clone())
589 {
590 self.pull_finished(&happened.repo_id, happened.number).await?;
591 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily592 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar593 "comment.created" => self.update_comment(event).await?,
594 // A pull request opened or its head moved: a dependency update
595 // file it changes is checked (not on `pull.ready`, which moves
596 // nothing), and dependency review runs.
597 "pull.opened" | "pull.updated" | "pull.ready" => {
598 if event.kind != "pull.ready" {
599 self.check_dependabot_file(event).await?;
600 }
601 if let Ok(happened) = serde_json::from_value::<PullHappened>(event.data.clone()) {
602 self.review_pull(&happened.repo_id, happened.number).await?;
603 }
604 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas605 "repo.created" => {
606 if let Ok(created) = serde_json::from_value::<Created>(event.data.clone()) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit607 // Recorded with its visibility, so the overview never takes a
608 // public repository for a private one.
609 if self.register_by_id(&created.repo_id).await?.is_some()
610 && let Some(private) = created.is_private
611 {
612 self.store.set_private(&created.repo_id, private).await?;
613 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas614 }
615 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar616 "repo.visibility_changed" => {
617 if let Ok(changed) = serde_json::from_value::<g1t_contracts::events::RepoVisibilityChanged>(event.data.clone())
618 && self.store.repo(&changed.repo_id).await?.is_some()
619 {
620 self.store.set_private(&changed.repo_id, changed.is_private).await?;
621 }
622 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas623 // A repository transferred or renamed: it is recorded under its new path.
624 "repo.transferred" | "repo.renamed" => {
625 if let Some(moved) = g1t_kit::transfer::read(event) {
626 // Where it is now, so moves heard out of order end in
627 // the same place.
628 let now: Option<g1t_contracts::repos::RepoPath> = g1t_kit::call(
629 &self.repos,
630 "path_by_id",
631 &g1t_contracts::repos::PathByIdArgs { id: moved.repo_id.clone() },
632 )
633 .await?;
634 let current = now.map_or_else(
635 || moved.destination().to_owned(),
636 |path| format!("{}/{}", path.namespace, path.name),
637 );
638 if let Some((namespace, name)) = current.split_once('/') {
639 self.store.moved(&moved.repo_id, namespace, name).await?;
640 }
641 }
642 }
643 // A repository purged: everything found in it goes.
644 "repo.purged" => {
645 if let Some(g1t_kit::lifecycle::Lifecycle::Purged(purged)) = g1t_kit::lifecycle::read(event) {
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9646 self.store.purge_advisories(&purged.repo_id).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar647 self.store.purge_suite(&purged.repo_id).await?;
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas648 self.store.purge(&purged.repo_id).await?;
649 }
650 }
651 "workspace.renamed" => {
652 if let Ok(renamed) = serde_json::from_value::<WorkspaceRenamed>(event.data.clone()) {
653 self.store.rename_namespace(&renamed.stale_slugs(&renamed.to), &renamed.to).await?;
654 }
655 }
656 _ => {}
657 }
658 Ok(())
659 }
660
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily661 /// The sweep: continues history scans and scans of pushes that landed
662 /// unscanned, catches security updates whose sandbox never pushed, and
663 /// reads dependencies that have not been read for a day.
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas664 async fn sweep(&self) -> Result<()> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily665 for scan in self.store.pending_push_scans(HISTORIES_PER_SWEEP).await? {
666 if let Err(error) = self.advance_push_scan(&scan.id, PAGES_PER_SWEEP).await {
667 worker::console_error!("security: push scan {} not continued: {error}", scan.id);
668 }
669 }
670 if let Err(error) = self.stalled_updates().await {
671 worker::console_error!("security: stalled security updates not handled: {error}");
672 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar673 if let Err(error) = self.sweep_suite().await {
674 worker::console_error!("security: daily snapshots and validity checks: {error}");
675 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas676 // Archived and deleted repositories wait; a few more are looked at
677 // so that they do not hold up the rest.
678 let mut histories = 0;
679 for repo in self.store.unfinished_histories(HISTORIES_PER_SWEEP * 4).await? {
680 if histories == HISTORIES_PER_SWEEP {
681 break;
682 }
683 if !self.active(&repo.repo_id).await? {
684 continue;
685 }
686 histories += 1;
687 if let Err(error) = self.advance_history(&repo, PAGES_PER_SWEEP).await {
688 worker::console_error!("security: history of {} not scanned: {error}", repo.repo_id);
689 }
690 }
691 let day_ago = rfc3339(now_ms().saturating_sub(DAY_MS));
692 for repo in self.store.stale_dependencies(&day_ago, DEPENDENCIES_PER_SWEEP).await? {
693 if !self.active(&repo.repo_id).await? {
694 self.store
695 .skip_dependencies(&repo.repo_id, "Dependencies are not checked while the repository is archived or deleted.")
696 .await?;
697 continue;
698 }
699 if let Err(error) = self.scan_dependencies(&repo).await {
700 worker::console_error!("security: dependencies of {} not read: {error}", repo.repo_id);
701 }
702 }
703 Ok(())
704 }
705}
706
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily707/// The fingerprints a push may carry: those someone dismissed (allowed),
708/// and likely test values, which are recorded but never stop a push.
709/// `known` is (fingerprint, id, status) of findings already recorded.
710fn let_through(known: &[(String, String, String)], secrets: &[NewSecret]) -> Vec<String> {
711 let mut allowed: Vec<String> = known
712 .iter()
713 .filter(|(_, _, status)| status == "allowed")
714 .map(|(fingerprint, _, _)| fingerprint.clone())
715 .chain(secrets.iter().filter(|secret| secret.test_value.is_some()).map(|secret| secret.fingerprint.clone()))
716 .collect();
717 allowed.sort();
718 allowed.dedup();
719 allowed
720}
721
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas722#[event(fetch)]
723async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
724 let Some(method) = rpc_method(&request) else {
725 return Response::error("Not found", 404);
726 };
727 let security = Security::new(&env)?;
728 let body: serde_json::Value = request.json().await?;
729 match method.as_str() {
730 "overview" => reply(&security.overview(args(body)?).await?),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily731 "dismiss" => reply(&security.dismiss(args(body)?).await?),
732 "reopen" => reply(&security.reopen(args(body)?).await?),
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas733 "rescan" => reply(&security.rescan(args(body)?).await?),
734 "set_upkeep" => reply(&security.set_upkeep(args(body)?).await?),
735 "workspace" => reply(&security.workspace(args(body)?).await?),
736 "push_blocked" => reply(&security.push_blocked(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar737 "check_updates" => reply(&security.check_updates(args(body)?).await?),
738 // The security suite.
739 "patterns_for" => reply(&security.patterns_for(args(body)?).await?),
740 "custom_patterns" => reply(&security.custom_patterns(args(body)?).await?),
741 "save_custom_pattern" => reply(&security.save_custom_pattern(args(body)?).await?),
742 "delete_custom_pattern" => reply(&security.delete_custom_pattern(args(body)?).await?),
743 "dry_run_pattern" => reply(&security.dry_run_pattern(args(body)?).await?),
744 "secret_alert" => reply(&security.secret_alert(args(body)?).await?),
745 "bypass" => reply(&security.bypass(args(body)?).await?),
746 "bypass_requests" => reply(&security.bypass_requests(args(body)?).await?),
747 "review_bypass" => reply(&security.review_bypass(args(body)?).await?),
748 "check_validity" => reply(&security.check_validity(args(body)?).await?),
749 "upload_sarif" => reply(&security.upload_sarif(args(body)?).await?),
750 "sarif_status" => reply(&security.sarif_status(args(body)?).await?),
751 "code_scanning" => reply(&security.code_scanning(args(body)?).await?),
752 "code_alert" => reply(&security.code_alert(args(body)?).await?),
753 "set_code_alert_state" => reply(&security.set_code_alert_state(args(body)?).await?),
754 "pull_code_scanning" => reply(&security.pull_code_scanning(args(body)?).await?),
755 "fix_alert" => reply(&security.fix_alert(args(body)?).await?),
756 "dependency_graph" => reply(&security.dependency_graph(args(body)?).await?),
757 "sbom" => reply(&security.sbom(args(body)?).await?),
758 "dependency_review" => reply(&security.dependency_review(args(body)?).await?),
759 "security_settings" => reply(&security.security_settings(args(body)?).await?),
760 "set_security_settings" => reply(&security.set_security_settings(args(body)?).await?),
761 "workspace_security_settings" => reply(&security.workspace_security_settings(args(body)?).await?),
762 "set_workspace_security_settings" => reply(&security.set_workspace_security_settings(args(body)?).await?),
763 "security_overview" => reply(&security.security_overview(args(body)?).await?),
764 "workspace_alerts" => reply(&security.workspace_alerts(args(body)?).await?),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9765 // Reporting: advisories and private vulnerability reports (advisories.rs).
766 "list_advisories" => reply(&security.list_advisories(args(body)?).await?),
767 "get_advisory" => reply(&security.get_advisory(args(body)?).await?),
768 "create_advisory" => reply(&security.create_advisory(args(body)?).await?),
769 "update_advisory" => reply(&security.update_advisory(args(body)?).await?),
770 "set_advisory_state" => reply(&security.set_advisory_state(args(body)?).await?),
771 "report_vulnerability" => reply(&security.report_vulnerability(args(body)?).await?),
772 "report_settings" => reply(&security.report_settings(args(body)?).await?),
773 "list_vulnerability_reports" => reply(&security.list_vulnerability_reports(args(body)?).await?),
774 "review_vulnerability_report" => reply(&security.review_vulnerability_report(args(body)?).await?),
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas775 _ => Response::error("Unknown method", 404),
776 }
777}
778
779#[event(queue)]
780async fn queue(batch: MessageBatch<Event>, env: Env, _ctx: Context) -> Result<()> {
781 let security = Security::new(&env)?;
782 for message in batch.messages()? {
783 let event = message.body();
784 if let Err(error) = security.on_event(event).await {
785 // Scans are idempotent and the sweep catches up, so one failed
786 // event is logged rather than retried.
787 worker::console_error!("security: {} {} failed: {error}", event.kind, event.id);
788 }
789 }
790 Ok(())
791}
792
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar793/// The cron (wrangler.jsonc) that runs version updates that are due.
794const VERSION_UPDATES_CRON: &str = "*/5 * * * *";
795
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas796#[event(scheduled)]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar797async fn scheduled(event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas798 match Security::new(&env) {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar799 // Version updates run every few minutes, so a schedule's time is kept.
800 Ok(security) if event.cron() == VERSION_UPDATES_CRON => {
801 if let Err(error) = security.version_update_sweep().await {
802 worker::console_error!("security: the version update sweep failed: {error}");
803 }
804 }
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas805 Ok(security) => {
806 if let Err(error) = security.sweep().await {
807 worker::console_error!("security: the sweep failed: {error}");
808 }
809 }
810 Err(error) => worker::console_error!("security: could not start: {error}"),
811 }
812}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily813
814#[cfg(test)]
815mod tests {
816 use super::*;
817
818 fn secret(fingerprint: &str, test_value: Option<&str>) -> NewSecret {
819 NewSecret {
820 fingerprint: fingerprint.into(),
821 kind: "aws_access_key".into(),
822 path: "a.env".into(),
823 line: 1,
824 commit: "c".into(),
825 preview: "AKIA…".into(),
826 test_value: test_value.map(str::to_owned),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar827 pattern_id: None,
828 pattern_name: None,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily829 }
830 }
831
832 #[test]
833 fn dismissed_secrets_and_test_values_go_through() {
834 let known = vec![
835 ("allowed".to_owned(), "sec_1".to_owned(), "allowed".to_owned()),
836 ("resolved".to_owned(), "sec_2".to_owned(), "resolved".to_owned()),
837 ("blocked".to_owned(), "sec_3".to_owned(), "blocked".to_owned()),
838 ];
839 let secrets = [secret("allowed", None), secret("resolved", None), secret("example", Some("it says it is an example")), secret("real", None)];
840 assert_eq!(let_through(&known, &secrets), ["allowed", "example"]);
841 }
842
843 #[test]
844 fn a_push_says_when_it_landed_unscanned() {
845 let pushed: Pushed = serde_json::from_value(serde_json::json!({
846 "repoId": "rep_1", "ref": "refs/heads/import", "after": "abc", "defaultBranch": false, "unscanned": true
847 }))
848 .unwrap();
849 assert!(pushed.unscanned && pushed.before.is_none() && pushed.git_ref == "refs/heads/import");
850 let ordinary: Pushed = serde_json::from_value(serde_json::json!({ "repoId": "rep_1", "ref": "refs/heads/main", "after": "abc", "defaultBranch": true })).unwrap();
851 assert!(!ordinary.unscanned);
852 }
853
854 #[test]
855 fn owners_are_told_what_landed_and_what_to_do() {
856 let one = history::landed_secrets_intro("acme", "rocket", "import", 1);
857 assert!(one.contains("A push to import in acme/rocket") && one.contains("a secret that looks real") && one.contains("Rotate"));
858 assert!(history::landed_secrets_intro("acme", "rocket", "main", 3).contains("3 secrets that look real"));
859 }
860
861 #[test]
862 fn dismissing_a_secret_takes_admin_and_a_dependency_write() {
Merge main (membership, two-factor, GitHub repo roles) into tokens863 assert_eq!(Security::capability_for("sec_1"), Capability::SecurityAlerts);
864 assert_eq!(Security::capability_for("vul_1"), Capability::SecurityAlerts);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily865 }
866}

This file's history is long; its oldest lines are credited to the oldest commit read.