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
847 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.

The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS1//! 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.
7//! 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.
12//! - Dependencies. On every push to a default branch, and daily, the
13//! lockfiles are read and every package checked against OSV. Each
14//! 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
17//! required checks. Only when code has to change is g1t put on an
18//! issue for it.
19//!
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//!
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS29//! 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;
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS35mod deps;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar36mod fixes;
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS37mod 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;
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS48mod security_updates;
49mod 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;
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS56mod updates;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar57mod version_updates;
58mod yaml;
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS59
60use g1t_contracts::access::{self, Capability};
61use g1t_contracts::events::{Event, WorkspaceRenamed};
62use g1t_contracts::security::UPDATE_BRANCH_PREFIX;
63use g1t_contracts::repos::{GetArgs, PathByIdArgs, Repo, RepoPath, RepoStatus, StatusByIdArgs};
64use g1t_contracts::security::*;
65use g1t_contracts::time::rfc3339;
66use g1t_contracts::{FailureCode, Outcome, User};
67use g1t_kit::{args, now_ms, reply, rpc_method};
68use serde::Deserialize;
69use worker::{Context, Env, Fetcher, MessageBatch, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
70
71use store::{Activity, RepoRow, Store};
72
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;
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS84
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>,
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS97}
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,
110 #[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,
119}
120
Security and version updates open no issues on their own: an update whose version cannot be raised fails with why on its alert and is tried again a day later, and one whose required checks fail stays open for a person to decide on; an agent takes one only when a person asks with Fix with g1t. That issue now names the lockfiles the package is locked in and the commands that show it is done, which were left empty ("Locked in: .") because the lockfiles were never passed in.121/// `pull.merged` and `pull.closed`, as far as this service reads them.
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS122#[derive(Deserialize)]
123#[serde(rename_all = "camelCase")]
124struct PullHappened {
125 repo_id: String,
126 number: u32,
127}
128
129/// The longest comment a dismissal keeps.
130const MAX_COMMENT_CHARS: usize = MAX_REASON_CHARS;
131/// Activity rows the Security page reads, newest first.
132const ACTIVITY_SHOWN: u32 = 500;
133
134#[derive(Deserialize)]
135#[serde(rename_all = "camelCase")]
136struct Created {
137 repo_id: String,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit138 /// Absent from events older than the field.
139 #[serde(default)]
140 is_private: Option<bool>,
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS141}
142
143impl Security {
144 fn new(env: &Env) -> Result<Self> {
145 Ok(Security {
146 store: Store { db: env.d1("DB")? },
147 identity: env.service("IDENTITY")?,
148 repos: env.service("REPOS")?,
149 work: env.service("WORK")?,
150 runner: env.service("RUNNER")?,
151 billing: env.service("BILLING")?,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar152 actions: env.service("ACTIONS")?,
153 events: env.service("EVENTS").ok(),
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS154 })
155 }
156
157 /// The repository at `path`, recorded here, if `viewer` may see its
158 /// findings (the Write role) and do `capability`. Findings are for those
159 /// who can change the code: to anyone else the page does not exist,
160 /// public repository or not.
161 async fn member_repo(
162 &self,
163 path: &RepoPath,
164 viewer: &Option<User>,
165 capability: Capability,
166 ) -> Result<Outcome<RepoRow>> {
167 let hidden = || fail(FailureCode::NotFound, "Repository not found.");
168 if viewer.is_none() {
169 return Ok(hidden());
170 }
171 let repo: Outcome<Repo> =
172 g1t_kit::call(&self.repos, "get", &GetArgs { path: path.clone(), viewer: viewer.clone() }).await?;
173 let repo = match repo {
174 Outcome::Ok(repo) if repo.fork_of.is_none() => repo,
175 _ => return Ok(hidden()),
176 };
177 if !access::can(viewer.as_ref(), &repo, SEE_FINDINGS) {
178 return Ok(hidden());
179 }
180 if !access::can(viewer.as_ref(), &repo, capability) {
181 return Ok(fail(
182 FailureCode::Forbidden,
183 access::needs(capability, &format!("{}/{}", repo.namespace, repo.name)),
184 ));
185 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar186 let row = self.store.register(&repo.id, &repo.namespace, &repo.name).await?;
187 self.store.set_private(&repo.id, repo.is_private).await?;
188 Ok(Outcome::Ok(row))
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS189 }
190
GitHub mirroring is gone: a repository comes across from GitHub by importing it, once, and is then its own on g1t. Standby mirrors, CI failover, takeovers, hand-backs, moving in and pushing back to GitHub made every part of g1t ask whether a repository leads, and that was more than the product wants to carry. Connecting a GitHub account or installing g1t's GitHub App, and importing repositories through it with every branch, tag and, if asked, their issues, work as before.191 /// Where the repository stands: archived or deleted. When repos
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.192 /// cannot say, it is taken as an ordinary active repository.
193 async fn status(&self, repo_id: &str) -> Result<RepoStatus> {
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS194 let status: Result<RepoStatus> =
195 g1t_kit::call(&self.repos, "status_by_id", &StatusByIdArgs { id: repo_id.to_owned() }).await;
196 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.197 Ok(status) => status,
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS198 Err(error) => {
199 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.200 RepoStatus::default()
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS201 }
202 })
203 }
204
GitHub mirroring is gone: a repository comes across from GitHub by importing it, once, and is then its own on g1t. Standby mirrors, CI failover, takeovers, hand-backs, moving in and pushing back to GitHub made every part of g1t ask whether a repository leads, and that was more than the product wants to carry. Connecting a GitHub account or installing g1t's GitHub App, and importing repositories through it with every branch, tag and, if asked, their issues, work as before.205 /// Whether the repository is neither archived nor deleted.
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.206 async fn active(&self, repo_id: &str) -> Result<bool> {
207 Ok(self.status(repo_id).await?.active())
208 }
209
210 /// Whether g1t may open branches and pull requests here on its own
GitHub mirroring is gone: a repository comes across from GitHub by importing it, once, and is then its own on g1t. Standby mirrors, CI failover, takeovers, hand-backs, moving in and pushing back to GitHub made every part of g1t ask whether a repository leads, and that was more than the product wants to carry. Connecting a GitHub account or installing g1t's GitHub App, and importing repositories through it with every branch, tag and, if asked, their issues, work as before.211 /// (security and dependency updates): it is active. `Err` says why
212 /// not.
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.213 async fn takes_updates(&self, repo: &RepoRow) -> Result<std::result::Result<(), String>> {
GitHub mirroring is gone: a repository comes across from GitHub by importing it, once, and is then its own on g1t. Standby mirrors, CI failover, takeovers, hand-backs, moving in and pushing back to GitHub made every part of g1t ask whether a repository leads, and that was more than the product wants to carry. Connecting a GitHub account or installing g1t's GitHub App, and importing repositories through it with every branch, tag and, if asked, their issues, work as before.214 if self.status(&repo.repo_id).await?.active() {
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.215 return Ok(Ok(()));
216 }
GitHub mirroring is gone: a repository comes across from GitHub by importing it, once, and is then its own on g1t. Standby mirrors, CI failover, takeovers, hand-backs, moving in and pushing back to GitHub made every part of g1t ask whether a repository leads, and that was more than the product wants to carry. Connecting a GitHub account or installing g1t's GitHub App, and importing repositories through it with every branch, tag and, if asked, their issues, work as before.217 Ok(Err("Updates wait while the repository is archived or deleted.".to_owned()))
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.218 }
219
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS220 /// Records a repository named in an event, by id. Forks are not
221 /// recorded: a pull request's findings belong to its repository.
222 async fn register_by_id(&self, repo_id: &str) -> Result<Option<RepoRow>> {
223 if let Some(row) = self.store.repo(repo_id).await? {
224 return Ok(Some(row));
225 }
226 let path: Option<RepoPath> = g1t_kit::call(&self.repos, "path_by_id", &PathByIdArgs { id: repo_id.to_owned() }).await?;
227 match path {
228 Some(path) => Ok(Some(self.store.register(repo_id, &path.namespace, &path.name).await?)),
229 None => Ok(None),
230 }
231 }
232
233 async fn overview(&self, a: OverviewArgs) -> Result<Outcome<SecurityOverview>> {
234 let mut repo = match self.member_repo(&a.repo, &a.viewer, SEE_FINDINGS).await? {
235 Outcome::Ok(repo) => repo,
236 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
237 };
238 // The first look at a repository reads its dependencies at once;
239 // its history is scanned in the background.
240 if repo.deps_scanned_at.is_none() {
241 self.scan_dependencies(&repo).await?;
242 repo = self.store.repo(&repo.repo_id).await?.unwrap_or(repo);
243 }
244 let (counts, _, _) = self.store.counts(&repo.repo_id).await?;
245 Ok(Outcome::Ok(SecurityOverview {
246 repo_id: repo.repo_id.clone(),
247 counts,
248 secret_counts: self.store.secret_counts(&repo.repo_id).await?,
249 secrets: self.store.secrets(&repo.repo_id).await?,
250 vulnerabilities: self.store.vulnerabilities(&repo.repo_id).await?,
251 activity: self.store.activity(&repo.repo_id, ACTIVITY_SHOWN).await?,
252 scan: repo.scan_state(),
253 upkeep: repo.upkeep != 0,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar254 version_updates: self.version_updates_view(&repo).await?,
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS255 }))
256 }
257
Merge main (membership, two-factor, GitHub repo roles) into tokens258 /// What dismissing or reopening alert `id` takes: Write, for a secret
259 /// as for a vulnerable dependency, as on GitHub; a security manager may
260 /// too.
261 fn capability_for(_id: &str) -> Capability {
262 SEE_FINDINGS
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS263 }
264
265 async fn dismiss(&self, a: DismissArgs) -> Result<Outcome<AlertChange>> {
266 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Self::capability_for(&a.id)).await? {
267 Outcome::Ok(repo) => repo,
268 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
269 };
270 if !a.actor.verified {
271 return Ok(fail(FailureCode::Forbidden, "Confirm your email address first."));
272 }
273 let comment: String = a.comment.trim().chars().take(MAX_COMMENT_CHARS).collect();
274 let comment = (!comment.is_empty()).then_some(comment);
275 if let Some(finding) = self.store.secret(&repo.repo_id, &a.id).await? {
276 if !a.reason.for_secrets() {
277 return Ok(fail(
278 FailureCode::Invalid,
279 "A secret is dismissed as false_positive, used_in_tests, revoked or wont_fix.",
280 ));
281 }
282 if finding.state != AlertState::Open {
283 return Ok(fail(FailureCode::Conflict, "This alert is not open. Reopen it first to dismiss it again."));
284 }
285 self.store
286 .dismiss_secret(&repo.repo_id, &a.id, a.reason, &a.actor.username, comment.as_deref())
287 .await?;
288 self.store
289 .record(&repo.repo_id, &[Activity {
290 alert_id: &a.id,
291 action: "dismissed",
292 actor: Some(&a.actor.username),
293 reason: Some(a.reason),
294 comment: comment.as_deref(),
295 number: None,
296 }])
297 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar298 let secret = self.store.secret(&repo.repo_id, &a.id).await?;
299 if let Some(secret) = &secret {
300 // Revoked is fixed; any other reason, dismissed.
301 let action = if a.reason == DismissReason::Revoked { "fixed" } else { "dismissed" };
302 let event = g1t_contracts::security_suite::SecurityEvent {
303 reason: Some(a.reason.as_str().to_owned()),
304 ..secret_alerts::secret_event(&repo, secret)
305 };
306 self.alert_event(g1t_contracts::security_suite::AlertType::SecretScanning, action, &repo, event, Some(a.actor.id.clone()))
307 .await;
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9308 let changed = summaries::Changed {
309 scan: "change",
310 fixed: if action == "fixed" { vec![a.id.clone()] } else { Vec::new() },
311 dismissed: u32::from(action == "dismissed"),
312 ..summaries::Changed::default()
313 };
314 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 bar315 }
316 return Ok(Outcome::Ok(AlertChange { secret, vulnerability: None }));
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS317 }
318 let Some(vuln) = self.store.vulnerability(&repo.repo_id, &a.id).await? else {
319 return Ok(fail(FailureCode::NotFound, "No such alert."));
320 };
321 if a.reason.for_secrets() {
322 return Ok(fail(
323 FailureCode::Invalid,
324 "A dependency is dismissed as fix_started, no_bandwidth, tolerable_risk, inaccurate or not_used.",
325 ));
326 }
327 if vuln.state != AlertState::Open {
328 return Ok(fail(FailureCode::Conflict, "This alert is not open. Reopen it first to dismiss it again."));
329 }
330 self.store
331 .dismiss_vulnerability(&repo.repo_id, &a.id, a.reason, &a.actor.username, comment.as_deref())
332 .await?;
333 self.store
334 .record(&repo.repo_id, &[Activity {
335 alert_id: &a.id,
336 action: "dismissed",
337 actor: Some(&a.actor.username),
338 reason: Some(a.reason),
339 comment: comment.as_deref(),
340 number: None,
341 }])
342 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar343 let vulnerability = self.store.vulnerability(&repo.repo_id, &a.id).await?;
344 if let Some(vuln) = &vulnerability {
345 let event = g1t_contracts::security_suite::SecurityEvent { reason: Some(a.reason.as_str().to_owned()), ..deps::vulnerability_event(&repo, vuln) };
346 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-7nm8r9347 let changed = summaries::Changed { scan: "change", dismissed: 1, ..summaries::Changed::default() };
348 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 bar349 }
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches350 // A security update whose alerts are now all fixed or dismissed closes.
351 if let Err(error) = self.resolve_updates(&repo, None).await {
352 worker::console_error!("security: updates of {} not resolved after a dismissal: {error}", repo.repo_id);
353 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar354 Ok(Outcome::Ok(AlertChange { secret: None, vulnerability }))
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS355 }
356
357 async fn reopen(&self, a: ReopenArgs) -> Result<Outcome<AlertChange>> {
358 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Self::capability_for(&a.id)).await? {
359 Outcome::Ok(repo) => repo,
360 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
361 };
362 if !a.actor.verified {
363 return Ok(fail(FailureCode::Forbidden, "Confirm your email address first."));
364 }
365 let reopened = Activity { alert_id: &a.id, action: "reopened", actor: Some(&a.actor.username), reason: None, comment: None, number: None };
366 if let Some(finding) = self.store.secret(&repo.repo_id, &a.id).await? {
367 if finding.state == AlertState::Open {
368 return Ok(fail(FailureCode::Conflict, "This alert is already open."));
369 }
370 // A secret that never landed has nothing to reopen to but blocked.
371 let status = if finding.source == "push" && finding.test_value.is_none() { SecretStatus::Blocked } else { SecretStatus::Open };
372 self.store.reopen_secret(&repo.repo_id, &a.id, status).await?;
373 self.store.record(&repo.repo_id, &[reopened]).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar374 let secret = self.store.secret(&repo.repo_id, &a.id).await?;
375 if let Some(secret) = &secret {
376 self.alert_event(
377 g1t_contracts::security_suite::AlertType::SecretScanning,
378 "reopened",
379 &repo,
380 secret_alerts::secret_event(&repo, secret),
381 Some(a.actor.id.clone()),
382 )
383 .await;
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9384 // The thread's counts move; nobody is told anew of what they reopened.
385 let changed = summaries::Changed { scan: "change", ..summaries::Changed::default() };
386 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 bar387 }
388 return Ok(Outcome::Ok(AlertChange { secret, vulnerability: None }));
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS389 }
390 let Some(vuln) = self.store.vulnerability(&repo.repo_id, &a.id).await? else {
391 return Ok(fail(FailureCode::NotFound, "No such alert."));
392 };
393 if vuln.state != AlertState::Dismissed {
394 return Ok(fail(FailureCode::Conflict, "Only a dismissed alert can be reopened; a fixed one reopens when it is found again."));
395 }
396 self.store.reopen_vulnerability(&repo.repo_id, &a.id).await?;
397 self.store.record(&repo.repo_id, &[reopened]).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar398 let vulnerability = self.store.vulnerability(&repo.repo_id, &a.id).await?;
399 if let Some(vuln) = &vulnerability {
400 self.alert_event(
401 g1t_contracts::security_suite::AlertType::Vulnerability,
402 "reopened",
403 &repo,
404 deps::vulnerability_event(&repo, vuln),
405 Some(a.actor.id.clone()),
406 )
407 .await;
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9408 let changed = summaries::Changed { scan: "change", ..summaries::Changed::default() };
409 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 bar410 }
411 Ok(Outcome::Ok(AlertChange { secret: None, vulnerability }))
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS412 }
413
414 async fn rescan(&self, a: RescanArgs) -> Result<Outcome<ScanState>> {
415 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), SEE_FINDINGS).await? {
416 Outcome::Ok(repo) => repo,
417 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
418 };
419 self.store.restart_history(&repo.repo_id).await?;
420 self.scan_dependencies(&repo).await?;
421 if let Some(fresh) = self.store.repo(&repo.repo_id).await? {
422 self.advance_history(&fresh, 1).await?;
423 }
424 let state = self.store.repo(&repo.repo_id).await?.map(|row| row.scan_state()).unwrap_or_default();
425 Ok(Outcome::Ok(state))
426 }
427
428 async fn set_upkeep(&self, a: SetUpkeepArgs) -> Result<Outcome<bool>> {
429 // Whether agents keep its dependencies up to date is one of its
430 // settings.
431 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Capability::ManageSettings).await? {
432 Outcome::Ok(repo) => repo,
433 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
434 };
435 if !a.actor.verified {
436 return Ok(fail(FailureCode::Forbidden, "Confirm your email address first."));
437 }
438 self.store.set_upkeep(&repo.repo_id, a.enabled, &a.actor.username).await?;
439 Ok(Outcome::Ok(a.enabled))
440 }
441
442 async fn workspace(&self, a: WorkspaceArgs) -> Result<Outcome<Vec<RepoSecurity>>> {
443 let workspace = a.workspace.to_lowercase();
444 if !a.viewer.as_ref().is_some_and(|user| user.is_member(&workspace)) {
445 return Ok(fail(FailureCode::NotFound, "Workspace not found."));
446 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit447 // Only the repositories whose findings the viewer may see. The
448 // Write role is never had through being public, so treating each
449 // as private changes nothing.
450 let repos: Vec<RepoRow> = self
451 .store
452 .in_namespace(&workspace)
453 .await?
454 .into_iter()
455 .filter(|repo| {
456 let target = access::RepoRef { id: &repo.repo_id, namespace: &workspace, private: true };
457 access::can(a.viewer.as_ref(), target, SEE_FINDINGS)
458 })
459 .collect();
460 // Every repository's counts at once, not one after another.
461 let counts = futures_util::future::try_join_all(repos.iter().map(|repo| self.store.counts(&repo.repo_id))).await?;
462 let list = repos
463 .into_iter()
464 .zip(counts)
465 .map(|(repo, (counts, secrets, vulnerabilities))| RepoSecurity {
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS466 repo_id: repo.repo_id,
467 name: repo.name,
468 counts,
469 secrets,
470 vulnerabilities,
471 upkeep: repo.upkeep != 0,
472 dependencies_scanned_at: repo.deps_scanned_at,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit473 })
474 .collect();
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS475 Ok(Outcome::Ok(list))
476 }
477
478 /// Push protection's question: which of these secrets were allowed?
479 /// The others are recorded as blocked, so someone can allow them.
480 async fn push_blocked(&self, a: PushBlockedArgs) -> Result<PushVerdict> {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar481 let repo = self.store.register(&a.repo_id, &a.path.namespace, &a.path.name).await?;
482 if let Some(private) = a.private {
483 self.store.set_private(&a.repo_id, private).await?;
484 }
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS485 let fingerprints: Vec<String> = a.secrets.iter().map(|secret| secret.fingerprint.clone()).collect();
486 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 bar487 let mut allowed = let_through(&known, &a.secrets);
488 // Bypassed with a reason: let through, whatever the alert says now.
489 allowed.extend(self.store.bypassed(&a.repo_id, &fingerprints).await?);
490 allowed.sort();
491 allowed.dedup();
492 let all = a.secrets.clone();
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS493 let fresh: Vec<NewSecret> = a
494 .secrets
495 .into_iter()
496 .filter(|secret| !known.iter().any(|(fingerprint, _, _)| *fingerprint == secret.fingerprint))
497 .collect();
498 // A likely test value goes through, so it lands: open, not blocked.
499 let (tests, real): (Vec<NewSecret>, Vec<NewSecret>) = fresh.into_iter().partition(|secret| secret.test_value.is_some());
500 self.store
501 .add_secrets(&a.repo_id, &real, SecretStatus::Blocked, "push", a.pusher.as_deref())
502 .await?;
503 self.store
504 .add_secrets(&a.repo_id, &tests, SecretStatus::Open, "push", a.pusher.as_deref())
505 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar506 let fresh: Vec<String> = real.iter().map(|secret| secret.fingerprint.clone()).collect();
507 self.secrets_found(&repo, &all, "push", &fresh, a.pusher.as_deref()).await?;
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS508 let ids = self
509 .store
510 .known(&a.repo_id, &fingerprints)
511 .await?
512 .into_iter()
513 .filter(|(fingerprint, _, _)| !allowed.contains(fingerprint))
514 .map(|(fingerprint, id, _)| (fingerprint, id))
515 .collect();
516 Ok(PushVerdict { allowed, ids })
517 }
518
519 /// What happens on the bus that concerns this service.
520 async fn on_event(&self, event: &Event) -> Result<()> {
521 match event.kind.as_str() {
522 "git.push" => {
523 let Ok(pushed) = serde_json::from_value::<Pushed>(event.data.clone()) else {
524 return Ok(());
525 };
526 // Too large to scan before it was stored: its new commits
527 // are scanned now, on whichever branch.
528 if pushed.unscanned
529 && !pushed.after.is_empty()
530 && let Some(repo) = self.register_by_id(&pushed.repo_id).await?
531 {
532 let id = self
533 .store
534 .add_push_scan(&repo.repo_id, &pushed.git_ref, &pushed.after, pushed.before.as_deref(), event.actor.as_deref())
535 .await?;
536 self.advance_push_scan(&id, history::PUSH_PAGES_AT_ONCE).await?;
537 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar538 // A version update's branch, or a grouped security update's,
539 // pushed by its sandbox: time for its pull request.
540 if !pushed.default_branch
541 && let Some(branch) = pushed.git_ref.strip_prefix("refs/heads/")
542 && self.update_pull_pushed(&pushed.repo_id, branch, &pushed.after).await?
543 {
544 return Ok(());
545 }
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS546 // A security update's branch, pushed by its sandbox: time for
547 // its pull request.
548 if let Some(branch) = pushed.git_ref.strip_prefix("refs/heads/")
549 && branch.starts_with(UPDATE_BRANCH_PREFIX)
550 {
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches551 self.update_pushed(&pushed.repo_id, branch, &pushed.after).await?;
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS552 return Ok(());
553 }
554 if !pushed.default_branch {
555 return Ok(());
556 }
557 if let Some(repo) = self.register_by_id(&pushed.repo_id).await? {
558 self.scan_dependencies(&repo).await?;
559 if repo.history != "done" {
560 self.advance_history(&repo, 1).await?;
561 }
562 }
563 }
Security and version updates open no issues on their own: an update whose version cannot be raised fails with why on its alert and is tried again a day later, and one whose required checks fail stays open for a person to decide on; an agent takes one only when a person asks with Fix with g1t. That issue now names the lockfiles the package is locked in and the commands that show it is done, which were left empty ("Locked in: .") because the lockfiles were never passed in.564 "pull.merged" | "pull.closed" => {
565 if let Ok(happened) = serde_json::from_value::<PullHappened>(event.data.clone()) {
566 if !self.update_pull_closed(&event.kind, &happened.repo_id, happened.number).await? {
567 self.update_pull_event(&event.kind, &happened.repo_id, happened.number, event.actor.as_deref()).await?;
568 }
569 // One of g1t's update pull requests closed or merged, by
570 // g1t or by a person: its branch goes.
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches571 self.pull_finished(&happened.repo_id, happened.number).await?;
572 }
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS573 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar574 "comment.created" => self.update_comment(event).await?,
575 // A pull request opened or its head moved: a dependency update
576 // file it changes is checked (not on `pull.ready`, which moves
577 // nothing), and dependency review runs.
578 "pull.opened" | "pull.updated" | "pull.ready" => {
579 if event.kind != "pull.ready" {
580 self.check_dependabot_file(event).await?;
581 }
582 if let Ok(happened) = serde_json::from_value::<PullHappened>(event.data.clone()) {
583 self.review_pull(&happened.repo_id, happened.number).await?;
584 }
585 }
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS586 "repo.created" => {
587 if let Ok(created) = serde_json::from_value::<Created>(event.data.clone()) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit588 // Recorded with its visibility, so the overview never takes a
589 // public repository for a private one.
590 if self.register_by_id(&created.repo_id).await?.is_some()
591 && let Some(private) = created.is_private
592 {
593 self.store.set_private(&created.repo_id, private).await?;
594 }
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS595 }
596 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar597 "repo.visibility_changed" => {
598 if let Ok(changed) = serde_json::from_value::<g1t_contracts::events::RepoVisibilityChanged>(event.data.clone())
599 && self.store.repo(&changed.repo_id).await?.is_some()
600 {
601 self.store.set_private(&changed.repo_id, changed.is_private).await?;
602 }
603 }
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS604 // A repository transferred or renamed: it is recorded under its new path.
605 "repo.transferred" | "repo.renamed" => {
606 if let Some(moved) = g1t_kit::transfer::read(event) {
607 // Where it is now, so moves heard out of order end in
608 // the same place.
609 let now: Option<g1t_contracts::repos::RepoPath> = g1t_kit::call(
610 &self.repos,
611 "path_by_id",
612 &g1t_contracts::repos::PathByIdArgs { id: moved.repo_id.clone() },
613 )
614 .await?;
615 let current = now.map_or_else(
616 || moved.destination().to_owned(),
617 |path| format!("{}/{}", path.namespace, path.name),
618 );
619 if let Some((namespace, name)) = current.split_once('/') {
620 self.store.moved(&moved.repo_id, namespace, name).await?;
621 }
622 }
623 }
624 // A repository purged: everything found in it goes.
625 "repo.purged" => {
626 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-7nm8r9627 self.store.purge_advisories(&purged.repo_id).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar628 self.store.purge_suite(&purged.repo_id).await?;
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS629 self.store.purge(&purged.repo_id).await?;
630 }
631 }
632 "workspace.renamed" => {
633 if let Ok(renamed) = serde_json::from_value::<WorkspaceRenamed>(event.data.clone()) {
634 self.store.rename_namespace(&renamed.stale_slugs(&renamed.to), &renamed.to).await?;
635 }
636 }
637 _ => {}
638 }
639 Ok(())
640 }
641
642 /// The sweep: continues history scans and scans of pushes that landed
643 /// unscanned, catches security updates whose sandbox never pushed, and
644 /// reads dependencies that have not been read for a day.
645 async fn sweep(&self) -> Result<()> {
646 for scan in self.store.pending_push_scans(HISTORIES_PER_SWEEP).await? {
647 if let Err(error) = self.advance_push_scan(&scan.id, PAGES_PER_SWEEP).await {
648 worker::console_error!("security: push scan {} not continued: {error}", scan.id);
649 }
650 }
651 if let Err(error) = self.stalled_updates().await {
652 worker::console_error!("security: stalled security updates not handled: {error}");
653 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar654 if let Err(error) = self.sweep_suite().await {
655 worker::console_error!("security: daily snapshots and validity checks: {error}");
656 }
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS657 // Archived and deleted repositories wait; a few more are looked at
658 // so that they do not hold up the rest.
659 let mut histories = 0;
660 for repo in self.store.unfinished_histories(HISTORIES_PER_SWEEP * 4).await? {
661 if histories == HISTORIES_PER_SWEEP {
662 break;
663 }
664 if !self.active(&repo.repo_id).await? {
665 continue;
666 }
667 histories += 1;
668 if let Err(error) = self.advance_history(&repo, PAGES_PER_SWEEP).await {
669 worker::console_error!("security: history of {} not scanned: {error}", repo.repo_id);
670 }
671 }
672 let day_ago = rfc3339(now_ms().saturating_sub(DAY_MS));
673 for repo in self.store.stale_dependencies(&day_ago, DEPENDENCIES_PER_SWEEP).await? {
674 if !self.active(&repo.repo_id).await? {
675 self.store
676 .skip_dependencies(&repo.repo_id, "Dependencies are not checked while the repository is archived or deleted.")
677 .await?;
678 continue;
679 }
680 if let Err(error) = self.scan_dependencies(&repo).await {
681 worker::console_error!("security: dependencies of {} not read: {error}", repo.repo_id);
682 }
683 }
684 Ok(())
685 }
686}
687
688/// The fingerprints a push may carry: those someone dismissed (allowed),
689/// and likely test values, which are recorded but never stop a push.
690/// `known` is (fingerprint, id, status) of findings already recorded.
691fn let_through(known: &[(String, String, String)], secrets: &[NewSecret]) -> Vec<String> {
692 let mut allowed: Vec<String> = known
693 .iter()
694 .filter(|(_, _, status)| status == "allowed")
695 .map(|(fingerprint, _, _)| fingerprint.clone())
696 .chain(secrets.iter().filter(|secret| secret.test_value.is_some()).map(|secret| secret.fingerprint.clone()))
697 .collect();
698 allowed.sort();
699 allowed.dedup();
700 allowed
701}
702
703#[event(fetch)]
704async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
705 let Some(method) = rpc_method(&request) else {
706 return Response::error("Not found", 404);
707 };
708 let security = Security::new(&env)?;
709 let body: serde_json::Value = request.json().await?;
710 match method.as_str() {
711 "overview" => reply(&security.overview(args(body)?).await?),
712 "dismiss" => reply(&security.dismiss(args(body)?).await?),
713 "reopen" => reply(&security.reopen(args(body)?).await?),
714 "rescan" => reply(&security.rescan(args(body)?).await?),
715 "set_upkeep" => reply(&security.set_upkeep(args(body)?).await?),
716 "workspace" => reply(&security.workspace(args(body)?).await?),
717 "push_blocked" => reply(&security.push_blocked(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar718 "check_updates" => reply(&security.check_updates(args(body)?).await?),
719 // The security suite.
720 "patterns_for" => reply(&security.patterns_for(args(body)?).await?),
721 "custom_patterns" => reply(&security.custom_patterns(args(body)?).await?),
722 "save_custom_pattern" => reply(&security.save_custom_pattern(args(body)?).await?),
723 "delete_custom_pattern" => reply(&security.delete_custom_pattern(args(body)?).await?),
724 "dry_run_pattern" => reply(&security.dry_run_pattern(args(body)?).await?),
725 "secret_alert" => reply(&security.secret_alert(args(body)?).await?),
726 "bypass" => reply(&security.bypass(args(body)?).await?),
727 "bypass_requests" => reply(&security.bypass_requests(args(body)?).await?),
728 "review_bypass" => reply(&security.review_bypass(args(body)?).await?),
729 "check_validity" => reply(&security.check_validity(args(body)?).await?),
730 "upload_sarif" => reply(&security.upload_sarif(args(body)?).await?),
731 "sarif_status" => reply(&security.sarif_status(args(body)?).await?),
732 "code_scanning" => reply(&security.code_scanning(args(body)?).await?),
733 "code_alert" => reply(&security.code_alert(args(body)?).await?),
734 "set_code_alert_state" => reply(&security.set_code_alert_state(args(body)?).await?),
735 "pull_code_scanning" => reply(&security.pull_code_scanning(args(body)?).await?),
736 "fix_alert" => reply(&security.fix_alert(args(body)?).await?),
737 "dependency_graph" => reply(&security.dependency_graph(args(body)?).await?),
738 "sbom" => reply(&security.sbom(args(body)?).await?),
739 "dependency_review" => reply(&security.dependency_review(args(body)?).await?),
740 "security_settings" => reply(&security.security_settings(args(body)?).await?),
741 "set_security_settings" => reply(&security.set_security_settings(args(body)?).await?),
742 "workspace_security_settings" => reply(&security.workspace_security_settings(args(body)?).await?),
743 "set_workspace_security_settings" => reply(&security.set_workspace_security_settings(args(body)?).await?),
744 "security_overview" => reply(&security.security_overview(args(body)?).await?),
745 "workspace_alerts" => reply(&security.workspace_alerts(args(body)?).await?),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9746 // Reporting: advisories and private vulnerability reports (advisories.rs).
747 "list_advisories" => reply(&security.list_advisories(args(body)?).await?),
748 "get_advisory" => reply(&security.get_advisory(args(body)?).await?),
749 "create_advisory" => reply(&security.create_advisory(args(body)?).await?),
750 "update_advisory" => reply(&security.update_advisory(args(body)?).await?),
751 "set_advisory_state" => reply(&security.set_advisory_state(args(body)?).await?),
752 "report_vulnerability" => reply(&security.report_vulnerability(args(body)?).await?),
753 "report_settings" => reply(&security.report_settings(args(body)?).await?),
754 "list_vulnerability_reports" => reply(&security.list_vulnerability_reports(args(body)?).await?),
755 "review_vulnerability_report" => reply(&security.review_vulnerability_report(args(body)?).await?),
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS756 _ => Response::error("Unknown method", 404),
757 }
758}
759
760#[event(queue)]
761async fn queue(batch: MessageBatch<Event>, env: Env, _ctx: Context) -> Result<()> {
762 let security = Security::new(&env)?;
763 for message in batch.messages()? {
764 let event = message.body();
765 if let Err(error) = security.on_event(event).await {
766 // Scans are idempotent and the sweep catches up, so one failed
767 // event is logged rather than retried.
768 worker::console_error!("security: {} {} failed: {error}", event.kind, event.id);
769 }
770 }
771 Ok(())
772}
773
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar774/// The cron (wrangler.jsonc) that runs version updates that are due.
775const VERSION_UPDATES_CRON: &str = "*/5 * * * *";
776
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS777#[event(scheduled)]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar778async fn scheduled(event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS779 match Security::new(&env) {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar780 // Version updates run every few minutes, so a schedule's time is kept.
781 Ok(security) if event.cron() == VERSION_UPDATES_CRON => {
782 if let Err(error) = security.version_update_sweep().await {
783 worker::console_error!("security: the version update sweep failed: {error}");
784 }
785 }
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS786 Ok(security) => {
787 if let Err(error) = security.sweep().await {
788 worker::console_error!("security: the sweep failed: {error}");
789 }
790 }
791 Err(error) => worker::console_error!("security: could not start: {error}"),
792 }
793}
794
795#[cfg(test)]
796mod tests {
797 use super::*;
798
799 fn secret(fingerprint: &str, test_value: Option<&str>) -> NewSecret {
800 NewSecret {
801 fingerprint: fingerprint.into(),
802 kind: "aws_access_key".into(),
803 path: "a.env".into(),
804 line: 1,
805 commit: "c".into(),
806 preview: "AKIA…".into(),
807 test_value: test_value.map(str::to_owned),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar808 pattern_id: None,
809 pattern_name: None,
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS810 }
811 }
812
813 #[test]
814 fn dismissed_secrets_and_test_values_go_through() {
815 let known = vec![
816 ("allowed".to_owned(), "sec_1".to_owned(), "allowed".to_owned()),
817 ("resolved".to_owned(), "sec_2".to_owned(), "resolved".to_owned()),
818 ("blocked".to_owned(), "sec_3".to_owned(), "blocked".to_owned()),
819 ];
820 let secrets = [secret("allowed", None), secret("resolved", None), secret("example", Some("it says it is an example")), secret("real", None)];
821 assert_eq!(let_through(&known, &secrets), ["allowed", "example"]);
822 }
823
824 #[test]
825 fn a_push_says_when_it_landed_unscanned() {
826 let pushed: Pushed = serde_json::from_value(serde_json::json!({
827 "repoId": "rep_1", "ref": "refs/heads/import", "after": "abc", "defaultBranch": false, "unscanned": true
828 }))
829 .unwrap();
830 assert!(pushed.unscanned && pushed.before.is_none() && pushed.git_ref == "refs/heads/import");
831 let ordinary: Pushed = serde_json::from_value(serde_json::json!({ "repoId": "rep_1", "ref": "refs/heads/main", "after": "abc", "defaultBranch": true })).unwrap();
832 assert!(!ordinary.unscanned);
833 }
834
835 #[test]
836 fn owners_are_told_what_landed_and_what_to_do() {
837 let one = history::landed_secrets_intro("acme", "rocket", "import", 1);
838 assert!(one.contains("A push to import in acme/rocket") && one.contains("a secret that looks real") && one.contains("Rotate"));
839 assert!(history::landed_secrets_intro("acme", "rocket", "main", 3).contains("3 secrets that look real"));
840 }
841
842 #[test]
843 fn dismissing_a_secret_takes_admin_and_a_dependency_write() {
Merge main (membership, two-factor, GitHub repo roles) into tokens844 assert_eq!(Security::capability_for("sec_1"), Capability::SecurityAlerts);
845 assert_eq!(Security::capability_for("vul_1"), Capability::SecurityAlerts);
The g1t CLI 0.1.0 is downloadable: g1t.sh/downloads/cli/latest/, five platforms with SHA256SUMS846 }
847}

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