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.

Pull requests: title and tabs across the full width, with counts and the size of the change1//! 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.
Pull requests: title and tabs across the full width, with counts and the size of the change12//! - 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.
Pull requests: title and tabs across the full width, with counts and the size of the change19//!
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//!
Pull requests: title and tabs across the full width, with counts and the size of the change29//! 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;
Pull requests: title and tabs across the full width, with counts and the size of the change35mod deps;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar36mod fixes;
Pull requests: title and tabs across the full width, with counts and the size of the change37mod 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;
Pull requests: title and tabs across the full width, with counts and the size of the change49mod 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;
Pull requests: title and tabs across the full width, with counts and the size of the change59
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look60use g1t_contracts::access::{self, Capability};
Pull requests: title and tabs across the full width, with counts and the size of the change61use 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;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look63use g1t_contracts::repos::{GetArgs, PathByIdArgs, Repo, RepoPath, RepoStatus, StatusByIdArgs};
Pull requests: title and tabs across the full width, with counts and the size of the change64use 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};
Pull requests: title and tabs across the full width, with counts and the size of the change67use 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};
Pull requests: title and tabs across the full width, with counts and the size of the change72
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;
Pull requests: title and tabs across the full width, with counts and the size of the change84
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>,
Pull requests: title and tabs across the full width, with counts and the size of the change97}
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,
Pull requests: title and tabs across the full width, with counts and the size of the change119}
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.
Pull requests: title and tabs across the full width, with counts and the size of the change123#[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")]
Pull requests: title and tabs across the full width, with counts and the size of the change139struct 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>,
Pull requests: title and tabs across the full width, with counts and the size of the change144}
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(),
Pull requests: title and tabs across the full width, with counts and the size of the change157 })
158 }
159
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look160 /// 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>> {
Pull requests: title and tabs across the full width, with counts and the size of the change170 let hidden = || fail(FailureCode::NotFound, "Repository not found.");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look171 if viewer.is_none() {
Pull requests: title and tabs across the full width, with counts and the size of the change172 return Ok(hidden());
173 }
174 let repo: Outcome<Repo> =
175 g1t_kit::call(&self.repos, "get", &GetArgs { path: path.clone(), viewer: viewer.clone() }).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look176 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());
Pull requests: title and tabs across the full width, with counts and the size of the change182 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look183 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))
Pull requests: title and tabs across the full width, with counts and the size of the change192 }
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> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look197 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,
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look201 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()
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look204 }
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
Pull requests: title and tabs across the full width, with counts and the size of the change228 /// 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>> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look242 let mut repo = match self.member_repo(&a.repo, &a.viewer, SEE_FINDINGS).await? {
Pull requests: title and tabs across the full width, with counts and the size of the change243 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?,
Pull requests: title and tabs across the full width, with counts and the size of the change257 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?,
Pull requests: title and tabs across the full width, with counts and the size of the change260 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?,
Pull requests: title and tabs across the full width, with counts and the size of the change263 }))
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? {
Pull requests: title and tabs across the full width, with counts and the size of the change275 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."));
Pull requests: title and tabs across the full width, with counts and the size of the change328 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily329 if a.reason.for_secrets() {
Pull requests: title and tabs across the full width, with counts and the size of the change330 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.",
Pull requests: title and tabs across the full width, with counts and the size of the change333 ));
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)),
Pull requests: title and tabs across the full width, with counts and the size of the change369 };
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."));
Pull requests: title and tabs across the full width, with counts and the size of the change400 };
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 }))
Pull requests: title and tabs across the full width, with counts and the size of the change420 }
421
422 async fn rescan(&self, a: RescanArgs) -> Result<Outcome<ScanState>> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look423 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), SEE_FINDINGS).await? {
Pull requests: title and tabs across the full width, with counts and the size of the change424 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>> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look437 // 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? {
Pull requests: title and tabs across the full width, with counts and the size of the change440 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 {
Pull requests: title and tabs across the full width, with counts and the size of the change474 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();
Pull requests: title and tabs across the full width, with counts and the size of the change483 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 }
Pull requests: title and tabs across the full width, with counts and the size of the change493 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();
Pull requests: title and tabs across the full width, with counts and the size of the change501 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());
Pull requests: title and tabs across the full width, with counts and the size of the change508 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())
Pull requests: title and tabs across the full width, with counts and the size of the change510 .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?;
Pull requests: title and tabs across the full width, with counts and the size of the change516 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 }
Pull requests: title and tabs across the full width, with counts and the size of the change562 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 }
Pull requests: title and tabs across the full width, with counts and the size of the change605 "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 }
Pull requests: title and tabs across the full width, with counts and the size of the change614 }
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 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look623 // 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?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look648 self.store.purge(&purged.repo_id).await?;
649 }
650 }
Pull requests: title and tabs across the full width, with counts and the size of the change651 "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.
Pull requests: title and tabs across the full width, with counts and the size of the change664 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 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look676 // 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;
Pull requests: title and tabs across the full width, with counts and the size of the change687 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? {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look693 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 }
Pull requests: title and tabs across the full width, with counts and the size of the change699 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
Pull requests: title and tabs across the full width, with counts and the size of the change722#[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?),
Pull requests: title and tabs across the full width, with counts and the size of the change733 "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?),
Pull requests: title and tabs across the full width, with counts and the size of the change775 _ => 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
Pull requests: title and tabs across the full width, with counts and the size of the change796#[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) {
Pull requests: title and tabs across the full width, with counts and the size of the change798 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 }
Pull requests: title and tabs across the full width, with counts and the size of the change805 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.