Skip to content
852 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.

Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays1//! 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
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
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays18//! 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//!
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays29//! 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;
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays35mod deps;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar36mod fixes;
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays37mod 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;
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays48mod 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;
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays56mod updates;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar57mod version_updates;
58mod yaml;
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays59
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;
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays84
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>,
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays97}
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.
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays122#[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>,
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays141}
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(),
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays154 })
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))
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays189 }
190
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.191 /// Where the repository stands: archived, deleted, a mirror. When repos
192 /// cannot say, it is taken as an ordinary active repository.
193 async fn status(&self, repo_id: &str) -> Result<RepoStatus> {
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays194 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,
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays198 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()
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays201 }
202 })
203 }
204
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.205 /// Whether the repository is neither archived nor deleted. Scanning
206 /// goes on in a mirror: it reads.
207 async fn active(&self, repo_id: &str) -> Result<bool> {
208 Ok(self.status(repo_id).await?.active())
209 }
210
211 /// Whether g1t may open branches and pull requests here on its own
212 /// (security and dependency updates): active, and not a mirror short
213 /// of a takeover (`g1t_contracts::mirrors`). `Err` says why not.
214 async fn takes_updates(&self, repo: &RepoRow) -> Result<std::result::Result<(), String>> {
215 let status = self.status(&repo.repo_id).await?;
216 if status.takes_updates() {
217 return Ok(Ok(()));
218 }
219 Ok(Err(match &status.mirror {
220 Some(mirror) if status.active() => g1t_contracts::mirrors::updates_wait_message(&repo.namespace, &repo.name, mirror),
221 _ => "Updates wait while the repository is archived or deleted.".to_owned(),
222 }))
223 }
224
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays225 /// Records a repository named in an event, by id. Forks are not
226 /// recorded: a pull request's findings belong to its repository.
227 async fn register_by_id(&self, repo_id: &str) -> Result<Option<RepoRow>> {
228 if let Some(row) = self.store.repo(repo_id).await? {
229 return Ok(Some(row));
230 }
231 let path: Option<RepoPath> = g1t_kit::call(&self.repos, "path_by_id", &PathByIdArgs { id: repo_id.to_owned() }).await?;
232 match path {
233 Some(path) => Ok(Some(self.store.register(repo_id, &path.namespace, &path.name).await?)),
234 None => Ok(None),
235 }
236 }
237
238 async fn overview(&self, a: OverviewArgs) -> Result<Outcome<SecurityOverview>> {
239 let mut repo = match self.member_repo(&a.repo, &a.viewer, SEE_FINDINGS).await? {
240 Outcome::Ok(repo) => repo,
241 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
242 };
243 // The first look at a repository reads its dependencies at once;
244 // its history is scanned in the background.
245 if repo.deps_scanned_at.is_none() {
246 self.scan_dependencies(&repo).await?;
247 repo = self.store.repo(&repo.repo_id).await?.unwrap_or(repo);
248 }
249 let (counts, _, _) = self.store.counts(&repo.repo_id).await?;
250 Ok(Outcome::Ok(SecurityOverview {
251 repo_id: repo.repo_id.clone(),
252 counts,
253 secret_counts: self.store.secret_counts(&repo.repo_id).await?,
254 secrets: self.store.secrets(&repo.repo_id).await?,
255 vulnerabilities: self.store.vulnerabilities(&repo.repo_id).await?,
256 activity: self.store.activity(&repo.repo_id, ACTIVITY_SHOWN).await?,
257 scan: repo.scan_state(),
258 upkeep: repo.upkeep != 0,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar259 version_updates: self.version_updates_view(&repo).await?,
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays260 }))
261 }
262
Merge main (membership, two-factor, GitHub repo roles) into tokens263 /// What dismissing or reopening alert `id` takes: Write, for a secret
264 /// as for a vulnerable dependency, as on GitHub; a security manager may
265 /// too.
266 fn capability_for(_id: &str) -> Capability {
267 SEE_FINDINGS
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays268 }
269
270 async fn dismiss(&self, a: DismissArgs) -> Result<Outcome<AlertChange>> {
271 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Self::capability_for(&a.id)).await? {
272 Outcome::Ok(repo) => repo,
273 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
274 };
275 if !a.actor.verified {
276 return Ok(fail(FailureCode::Forbidden, "Confirm your email address first."));
277 }
278 let comment: String = a.comment.trim().chars().take(MAX_COMMENT_CHARS).collect();
279 let comment = (!comment.is_empty()).then_some(comment);
280 if let Some(finding) = self.store.secret(&repo.repo_id, &a.id).await? {
281 if !a.reason.for_secrets() {
282 return Ok(fail(
283 FailureCode::Invalid,
284 "A secret is dismissed as false_positive, used_in_tests, revoked or wont_fix.",
285 ));
286 }
287 if finding.state != AlertState::Open {
288 return Ok(fail(FailureCode::Conflict, "This alert is not open. Reopen it first to dismiss it again."));
289 }
290 self.store
291 .dismiss_secret(&repo.repo_id, &a.id, a.reason, &a.actor.username, comment.as_deref())
292 .await?;
293 self.store
294 .record(&repo.repo_id, &[Activity {
295 alert_id: &a.id,
296 action: "dismissed",
297 actor: Some(&a.actor.username),
298 reason: Some(a.reason),
299 comment: comment.as_deref(),
300 number: None,
301 }])
302 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar303 let secret = self.store.secret(&repo.repo_id, &a.id).await?;
304 if let Some(secret) = &secret {
305 // Revoked is fixed; any other reason, dismissed.
306 let action = if a.reason == DismissReason::Revoked { "fixed" } else { "dismissed" };
307 let event = g1t_contracts::security_suite::SecurityEvent {
308 reason: Some(a.reason.as_str().to_owned()),
309 ..secret_alerts::secret_event(&repo, secret)
310 };
311 self.alert_event(g1t_contracts::security_suite::AlertType::SecretScanning, action, &repo, event, Some(a.actor.id.clone()))
312 .await;
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9313 let changed = summaries::Changed {
314 scan: "change",
315 fixed: if action == "fixed" { vec![a.id.clone()] } else { Vec::new() },
316 dismissed: u32::from(action == "dismissed"),
317 ..summaries::Changed::default()
318 };
319 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 bar320 }
321 return Ok(Outcome::Ok(AlertChange { secret, vulnerability: None }));
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays322 }
323 let Some(vuln) = self.store.vulnerability(&repo.repo_id, &a.id).await? else {
324 return Ok(fail(FailureCode::NotFound, "No such alert."));
325 };
326 if a.reason.for_secrets() {
327 return Ok(fail(
328 FailureCode::Invalid,
329 "A dependency is dismissed as fix_started, no_bandwidth, tolerable_risk, inaccurate or not_used.",
330 ));
331 }
332 if vuln.state != AlertState::Open {
333 return Ok(fail(FailureCode::Conflict, "This alert is not open. Reopen it first to dismiss it again."));
334 }
335 self.store
336 .dismiss_vulnerability(&repo.repo_id, &a.id, a.reason, &a.actor.username, comment.as_deref())
337 .await?;
338 self.store
339 .record(&repo.repo_id, &[Activity {
340 alert_id: &a.id,
341 action: "dismissed",
342 actor: Some(&a.actor.username),
343 reason: Some(a.reason),
344 comment: comment.as_deref(),
345 number: None,
346 }])
347 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar348 let vulnerability = self.store.vulnerability(&repo.repo_id, &a.id).await?;
349 if let Some(vuln) = &vulnerability {
350 let event = g1t_contracts::security_suite::SecurityEvent { reason: Some(a.reason.as_str().to_owned()), ..deps::vulnerability_event(&repo, vuln) };
351 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-7nm8r9352 let changed = summaries::Changed { scan: "change", dismissed: 1, ..summaries::Changed::default() };
353 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 bar354 }
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches355 // A security update whose alerts are now all fixed or dismissed closes.
356 if let Err(error) = self.resolve_updates(&repo, None).await {
357 worker::console_error!("security: updates of {} not resolved after a dismissal: {error}", repo.repo_id);
358 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar359 Ok(Outcome::Ok(AlertChange { secret: None, vulnerability }))
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays360 }
361
362 async fn reopen(&self, a: ReopenArgs) -> Result<Outcome<AlertChange>> {
363 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Self::capability_for(&a.id)).await? {
364 Outcome::Ok(repo) => repo,
365 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
366 };
367 if !a.actor.verified {
368 return Ok(fail(FailureCode::Forbidden, "Confirm your email address first."));
369 }
370 let reopened = Activity { alert_id: &a.id, action: "reopened", actor: Some(&a.actor.username), reason: None, comment: None, number: None };
371 if let Some(finding) = self.store.secret(&repo.repo_id, &a.id).await? {
372 if finding.state == AlertState::Open {
373 return Ok(fail(FailureCode::Conflict, "This alert is already open."));
374 }
375 // A secret that never landed has nothing to reopen to but blocked.
376 let status = if finding.source == "push" && finding.test_value.is_none() { SecretStatus::Blocked } else { SecretStatus::Open };
377 self.store.reopen_secret(&repo.repo_id, &a.id, status).await?;
378 self.store.record(&repo.repo_id, &[reopened]).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar379 let secret = self.store.secret(&repo.repo_id, &a.id).await?;
380 if let Some(secret) = &secret {
381 self.alert_event(
382 g1t_contracts::security_suite::AlertType::SecretScanning,
383 "reopened",
384 &repo,
385 secret_alerts::secret_event(&repo, secret),
386 Some(a.actor.id.clone()),
387 )
388 .await;
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9389 // The thread's counts move; nobody is told anew of what they reopened.
390 let changed = summaries::Changed { scan: "change", ..summaries::Changed::default() };
391 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 bar392 }
393 return Ok(Outcome::Ok(AlertChange { secret, vulnerability: None }));
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays394 }
395 let Some(vuln) = self.store.vulnerability(&repo.repo_id, &a.id).await? else {
396 return Ok(fail(FailureCode::NotFound, "No such alert."));
397 };
398 if vuln.state != AlertState::Dismissed {
399 return Ok(fail(FailureCode::Conflict, "Only a dismissed alert can be reopened; a fixed one reopens when it is found again."));
400 }
401 self.store.reopen_vulnerability(&repo.repo_id, &a.id).await?;
402 self.store.record(&repo.repo_id, &[reopened]).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar403 let vulnerability = self.store.vulnerability(&repo.repo_id, &a.id).await?;
404 if let Some(vuln) = &vulnerability {
405 self.alert_event(
406 g1t_contracts::security_suite::AlertType::Vulnerability,
407 "reopened",
408 &repo,
409 deps::vulnerability_event(&repo, vuln),
410 Some(a.actor.id.clone()),
411 )
412 .await;
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9413 let changed = summaries::Changed { scan: "change", ..summaries::Changed::default() };
414 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 bar415 }
416 Ok(Outcome::Ok(AlertChange { secret: None, vulnerability }))
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays417 }
418
419 async fn rescan(&self, a: RescanArgs) -> Result<Outcome<ScanState>> {
420 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), SEE_FINDINGS).await? {
421 Outcome::Ok(repo) => repo,
422 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
423 };
424 self.store.restart_history(&repo.repo_id).await?;
425 self.scan_dependencies(&repo).await?;
426 if let Some(fresh) = self.store.repo(&repo.repo_id).await? {
427 self.advance_history(&fresh, 1).await?;
428 }
429 let state = self.store.repo(&repo.repo_id).await?.map(|row| row.scan_state()).unwrap_or_default();
430 Ok(Outcome::Ok(state))
431 }
432
433 async fn set_upkeep(&self, a: SetUpkeepArgs) -> Result<Outcome<bool>> {
434 // Whether agents keep its dependencies up to date is one of its
435 // settings.
436 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Capability::ManageSettings).await? {
437 Outcome::Ok(repo) => repo,
438 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
439 };
440 if !a.actor.verified {
441 return Ok(fail(FailureCode::Forbidden, "Confirm your email address first."));
442 }
443 self.store.set_upkeep(&repo.repo_id, a.enabled, &a.actor.username).await?;
444 Ok(Outcome::Ok(a.enabled))
445 }
446
447 async fn workspace(&self, a: WorkspaceArgs) -> Result<Outcome<Vec<RepoSecurity>>> {
448 let workspace = a.workspace.to_lowercase();
449 if !a.viewer.as_ref().is_some_and(|user| user.is_member(&workspace)) {
450 return Ok(fail(FailureCode::NotFound, "Workspace not found."));
451 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit452 // Only the repositories whose findings the viewer may see. The
453 // Write role is never had through being public, so treating each
454 // as private changes nothing.
455 let repos: Vec<RepoRow> = self
456 .store
457 .in_namespace(&workspace)
458 .await?
459 .into_iter()
460 .filter(|repo| {
461 let target = access::RepoRef { id: &repo.repo_id, namespace: &workspace, private: true };
462 access::can(a.viewer.as_ref(), target, SEE_FINDINGS)
463 })
464 .collect();
465 // Every repository's counts at once, not one after another.
466 let counts = futures_util::future::try_join_all(repos.iter().map(|repo| self.store.counts(&repo.repo_id))).await?;
467 let list = repos
468 .into_iter()
469 .zip(counts)
470 .map(|(repo, (counts, secrets, vulnerabilities))| RepoSecurity {
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays471 repo_id: repo.repo_id,
472 name: repo.name,
473 counts,
474 secrets,
475 vulnerabilities,
476 upkeep: repo.upkeep != 0,
477 dependencies_scanned_at: repo.deps_scanned_at,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit478 })
479 .collect();
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays480 Ok(Outcome::Ok(list))
481 }
482
483 /// Push protection's question: which of these secrets were allowed?
484 /// The others are recorded as blocked, so someone can allow them.
485 async fn push_blocked(&self, a: PushBlockedArgs) -> Result<PushVerdict> {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar486 let repo = self.store.register(&a.repo_id, &a.path.namespace, &a.path.name).await?;
487 if let Some(private) = a.private {
488 self.store.set_private(&a.repo_id, private).await?;
489 }
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays490 let fingerprints: Vec<String> = a.secrets.iter().map(|secret| secret.fingerprint.clone()).collect();
491 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 bar492 let mut allowed = let_through(&known, &a.secrets);
493 // Bypassed with a reason: let through, whatever the alert says now.
494 allowed.extend(self.store.bypassed(&a.repo_id, &fingerprints).await?);
495 allowed.sort();
496 allowed.dedup();
497 let all = a.secrets.clone();
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays498 let fresh: Vec<NewSecret> = a
499 .secrets
500 .into_iter()
501 .filter(|secret| !known.iter().any(|(fingerprint, _, _)| *fingerprint == secret.fingerprint))
502 .collect();
503 // A likely test value goes through, so it lands: open, not blocked.
504 let (tests, real): (Vec<NewSecret>, Vec<NewSecret>) = fresh.into_iter().partition(|secret| secret.test_value.is_some());
505 self.store
506 .add_secrets(&a.repo_id, &real, SecretStatus::Blocked, "push", a.pusher.as_deref())
507 .await?;
508 self.store
509 .add_secrets(&a.repo_id, &tests, SecretStatus::Open, "push", a.pusher.as_deref())
510 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar511 let fresh: Vec<String> = real.iter().map(|secret| secret.fingerprint.clone()).collect();
512 self.secrets_found(&repo, &all, "push", &fresh, a.pusher.as_deref()).await?;
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays513 let ids = self
514 .store
515 .known(&a.repo_id, &fingerprints)
516 .await?
517 .into_iter()
518 .filter(|(fingerprint, _, _)| !allowed.contains(fingerprint))
519 .map(|(fingerprint, id, _)| (fingerprint, id))
520 .collect();
521 Ok(PushVerdict { allowed, ids })
522 }
523
524 /// What happens on the bus that concerns this service.
525 async fn on_event(&self, event: &Event) -> Result<()> {
526 match event.kind.as_str() {
527 "git.push" => {
528 let Ok(pushed) = serde_json::from_value::<Pushed>(event.data.clone()) else {
529 return Ok(());
530 };
531 // Too large to scan before it was stored: its new commits
532 // are scanned now, on whichever branch.
533 if pushed.unscanned
534 && !pushed.after.is_empty()
535 && let Some(repo) = self.register_by_id(&pushed.repo_id).await?
536 {
537 let id = self
538 .store
539 .add_push_scan(&repo.repo_id, &pushed.git_ref, &pushed.after, pushed.before.as_deref(), event.actor.as_deref())
540 .await?;
541 self.advance_push_scan(&id, history::PUSH_PAGES_AT_ONCE).await?;
542 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar543 // A version update's branch, or a grouped security update's,
544 // pushed by its sandbox: time for its pull request.
545 if !pushed.default_branch
546 && let Some(branch) = pushed.git_ref.strip_prefix("refs/heads/")
547 && self.update_pull_pushed(&pushed.repo_id, branch, &pushed.after).await?
548 {
549 return Ok(());
550 }
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays551 // A security update's branch, pushed by its sandbox: time for
552 // its pull request.
553 if let Some(branch) = pushed.git_ref.strip_prefix("refs/heads/")
554 && branch.starts_with(UPDATE_BRANCH_PREFIX)
555 {
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches556 self.update_pushed(&pushed.repo_id, branch, &pushed.after).await?;
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays557 return Ok(());
558 }
559 if !pushed.default_branch {
560 return Ok(());
561 }
562 if let Some(repo) = self.register_by_id(&pushed.repo_id).await? {
563 self.scan_dependencies(&repo).await?;
564 if repo.history != "done" {
565 self.advance_history(&repo, 1).await?;
566 }
567 }
568 }
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.569 "pull.merged" | "pull.closed" => {
570 if let Ok(happened) = serde_json::from_value::<PullHappened>(event.data.clone()) {
571 if !self.update_pull_closed(&event.kind, &happened.repo_id, happened.number).await? {
572 self.update_pull_event(&event.kind, &happened.repo_id, happened.number, event.actor.as_deref()).await?;
573 }
574 // One of g1t's update pull requests closed or merged, by
575 // 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 branches576 self.pull_finished(&happened.repo_id, happened.number).await?;
577 }
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays578 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar579 "comment.created" => self.update_comment(event).await?,
580 // A pull request opened or its head moved: a dependency update
581 // file it changes is checked (not on `pull.ready`, which moves
582 // nothing), and dependency review runs.
583 "pull.opened" | "pull.updated" | "pull.ready" => {
584 if event.kind != "pull.ready" {
585 self.check_dependabot_file(event).await?;
586 }
587 if let Ok(happened) = serde_json::from_value::<PullHappened>(event.data.clone()) {
588 self.review_pull(&happened.repo_id, happened.number).await?;
589 }
590 }
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays591 "repo.created" => {
592 if let Ok(created) = serde_json::from_value::<Created>(event.data.clone()) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit593 // Recorded with its visibility, so the overview never takes a
594 // public repository for a private one.
595 if self.register_by_id(&created.repo_id).await?.is_some()
596 && let Some(private) = created.is_private
597 {
598 self.store.set_private(&created.repo_id, private).await?;
599 }
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays600 }
601 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar602 "repo.visibility_changed" => {
603 if let Ok(changed) = serde_json::from_value::<g1t_contracts::events::RepoVisibilityChanged>(event.data.clone())
604 && self.store.repo(&changed.repo_id).await?.is_some()
605 {
606 self.store.set_private(&changed.repo_id, changed.is_private).await?;
607 }
608 }
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays609 // A repository transferred or renamed: it is recorded under its new path.
610 "repo.transferred" | "repo.renamed" => {
611 if let Some(moved) = g1t_kit::transfer::read(event) {
612 // Where it is now, so moves heard out of order end in
613 // the same place.
614 let now: Option<g1t_contracts::repos::RepoPath> = g1t_kit::call(
615 &self.repos,
616 "path_by_id",
617 &g1t_contracts::repos::PathByIdArgs { id: moved.repo_id.clone() },
618 )
619 .await?;
620 let current = now.map_or_else(
621 || moved.destination().to_owned(),
622 |path| format!("{}/{}", path.namespace, path.name),
623 );
624 if let Some((namespace, name)) = current.split_once('/') {
625 self.store.moved(&moved.repo_id, namespace, name).await?;
626 }
627 }
628 }
629 // A repository purged: everything found in it goes.
630 "repo.purged" => {
631 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-7nm8r9632 self.store.purge_advisories(&purged.repo_id).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar633 self.store.purge_suite(&purged.repo_id).await?;
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays634 self.store.purge(&purged.repo_id).await?;
635 }
636 }
637 "workspace.renamed" => {
638 if let Ok(renamed) = serde_json::from_value::<WorkspaceRenamed>(event.data.clone()) {
639 self.store.rename_namespace(&renamed.stale_slugs(&renamed.to), &renamed.to).await?;
640 }
641 }
642 _ => {}
643 }
644 Ok(())
645 }
646
647 /// The sweep: continues history scans and scans of pushes that landed
648 /// unscanned, catches security updates whose sandbox never pushed, and
649 /// reads dependencies that have not been read for a day.
650 async fn sweep(&self) -> Result<()> {
651 for scan in self.store.pending_push_scans(HISTORIES_PER_SWEEP).await? {
652 if let Err(error) = self.advance_push_scan(&scan.id, PAGES_PER_SWEEP).await {
653 worker::console_error!("security: push scan {} not continued: {error}", scan.id);
654 }
655 }
656 if let Err(error) = self.stalled_updates().await {
657 worker::console_error!("security: stalled security updates not handled: {error}");
658 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar659 if let Err(error) = self.sweep_suite().await {
660 worker::console_error!("security: daily snapshots and validity checks: {error}");
661 }
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays662 // Archived and deleted repositories wait; a few more are looked at
663 // so that they do not hold up the rest.
664 let mut histories = 0;
665 for repo in self.store.unfinished_histories(HISTORIES_PER_SWEEP * 4).await? {
666 if histories == HISTORIES_PER_SWEEP {
667 break;
668 }
669 if !self.active(&repo.repo_id).await? {
670 continue;
671 }
672 histories += 1;
673 if let Err(error) = self.advance_history(&repo, PAGES_PER_SWEEP).await {
674 worker::console_error!("security: history of {} not scanned: {error}", repo.repo_id);
675 }
676 }
677 let day_ago = rfc3339(now_ms().saturating_sub(DAY_MS));
678 for repo in self.store.stale_dependencies(&day_ago, DEPENDENCIES_PER_SWEEP).await? {
679 if !self.active(&repo.repo_id).await? {
680 self.store
681 .skip_dependencies(&repo.repo_id, "Dependencies are not checked while the repository is archived or deleted.")
682 .await?;
683 continue;
684 }
685 if let Err(error) = self.scan_dependencies(&repo).await {
686 worker::console_error!("security: dependencies of {} not read: {error}", repo.repo_id);
687 }
688 }
689 Ok(())
690 }
691}
692
693/// The fingerprints a push may carry: those someone dismissed (allowed),
694/// and likely test values, which are recorded but never stop a push.
695/// `known` is (fingerprint, id, status) of findings already recorded.
696fn let_through(known: &[(String, String, String)], secrets: &[NewSecret]) -> Vec<String> {
697 let mut allowed: Vec<String> = known
698 .iter()
699 .filter(|(_, _, status)| status == "allowed")
700 .map(|(fingerprint, _, _)| fingerprint.clone())
701 .chain(secrets.iter().filter(|secret| secret.test_value.is_some()).map(|secret| secret.fingerprint.clone()))
702 .collect();
703 allowed.sort();
704 allowed.dedup();
705 allowed
706}
707
708#[event(fetch)]
709async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
710 let Some(method) = rpc_method(&request) else {
711 return Response::error("Not found", 404);
712 };
713 let security = Security::new(&env)?;
714 let body: serde_json::Value = request.json().await?;
715 match method.as_str() {
716 "overview" => reply(&security.overview(args(body)?).await?),
717 "dismiss" => reply(&security.dismiss(args(body)?).await?),
718 "reopen" => reply(&security.reopen(args(body)?).await?),
719 "rescan" => reply(&security.rescan(args(body)?).await?),
720 "set_upkeep" => reply(&security.set_upkeep(args(body)?).await?),
721 "workspace" => reply(&security.workspace(args(body)?).await?),
722 "push_blocked" => reply(&security.push_blocked(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar723 "check_updates" => reply(&security.check_updates(args(body)?).await?),
724 // The security suite.
725 "patterns_for" => reply(&security.patterns_for(args(body)?).await?),
726 "custom_patterns" => reply(&security.custom_patterns(args(body)?).await?),
727 "save_custom_pattern" => reply(&security.save_custom_pattern(args(body)?).await?),
728 "delete_custom_pattern" => reply(&security.delete_custom_pattern(args(body)?).await?),
729 "dry_run_pattern" => reply(&security.dry_run_pattern(args(body)?).await?),
730 "secret_alert" => reply(&security.secret_alert(args(body)?).await?),
731 "bypass" => reply(&security.bypass(args(body)?).await?),
732 "bypass_requests" => reply(&security.bypass_requests(args(body)?).await?),
733 "review_bypass" => reply(&security.review_bypass(args(body)?).await?),
734 "check_validity" => reply(&security.check_validity(args(body)?).await?),
735 "upload_sarif" => reply(&security.upload_sarif(args(body)?).await?),
736 "sarif_status" => reply(&security.sarif_status(args(body)?).await?),
737 "code_scanning" => reply(&security.code_scanning(args(body)?).await?),
738 "code_alert" => reply(&security.code_alert(args(body)?).await?),
739 "set_code_alert_state" => reply(&security.set_code_alert_state(args(body)?).await?),
740 "pull_code_scanning" => reply(&security.pull_code_scanning(args(body)?).await?),
741 "fix_alert" => reply(&security.fix_alert(args(body)?).await?),
742 "dependency_graph" => reply(&security.dependency_graph(args(body)?).await?),
743 "sbom" => reply(&security.sbom(args(body)?).await?),
744 "dependency_review" => reply(&security.dependency_review(args(body)?).await?),
745 "security_settings" => reply(&security.security_settings(args(body)?).await?),
746 "set_security_settings" => reply(&security.set_security_settings(args(body)?).await?),
747 "workspace_security_settings" => reply(&security.workspace_security_settings(args(body)?).await?),
748 "set_workspace_security_settings" => reply(&security.set_workspace_security_settings(args(body)?).await?),
749 "security_overview" => reply(&security.security_overview(args(body)?).await?),
750 "workspace_alerts" => reply(&security.workspace_alerts(args(body)?).await?),
Merge remote-tracking branch 'origin/claude/cool-newton-7nm8r9' into claude/cool-newton-7nm8r9751 // Reporting: advisories and private vulnerability reports (advisories.rs).
752 "list_advisories" => reply(&security.list_advisories(args(body)?).await?),
753 "get_advisory" => reply(&security.get_advisory(args(body)?).await?),
754 "create_advisory" => reply(&security.create_advisory(args(body)?).await?),
755 "update_advisory" => reply(&security.update_advisory(args(body)?).await?),
756 "set_advisory_state" => reply(&security.set_advisory_state(args(body)?).await?),
757 "report_vulnerability" => reply(&security.report_vulnerability(args(body)?).await?),
758 "report_settings" => reply(&security.report_settings(args(body)?).await?),
759 "list_vulnerability_reports" => reply(&security.list_vulnerability_reports(args(body)?).await?),
760 "review_vulnerability_report" => reply(&security.review_vulnerability_report(args(body)?).await?),
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays761 _ => Response::error("Unknown method", 404),
762 }
763}
764
765#[event(queue)]
766async fn queue(batch: MessageBatch<Event>, env: Env, _ctx: Context) -> Result<()> {
767 let security = Security::new(&env)?;
768 for message in batch.messages()? {
769 let event = message.body();
770 if let Err(error) = security.on_event(event).await {
771 // Scans are idempotent and the sweep catches up, so one failed
772 // event is logged rather than retried.
773 worker::console_error!("security: {} {} failed: {error}", event.kind, event.id);
774 }
775 }
776 Ok(())
777}
778
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar779/// The cron (wrangler.jsonc) that runs version updates that are due.
780const VERSION_UPDATES_CRON: &str = "*/5 * * * *";
781
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays782#[event(scheduled)]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar783async fn scheduled(event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays784 match Security::new(&env) {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar785 // Version updates run every few minutes, so a schedule's time is kept.
786 Ok(security) if event.cron() == VERSION_UPDATES_CRON => {
787 if let Err(error) = security.version_update_sweep().await {
788 worker::console_error!("security: the version update sweep failed: {error}");
789 }
790 }
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays791 Ok(security) => {
792 if let Err(error) = security.sweep().await {
793 worker::console_error!("security: the sweep failed: {error}");
794 }
795 }
796 Err(error) => worker::console_error!("security: could not start: {error}"),
797 }
798}
799
800#[cfg(test)]
801mod tests {
802 use super::*;
803
804 fn secret(fingerprint: &str, test_value: Option<&str>) -> NewSecret {
805 NewSecret {
806 fingerprint: fingerprint.into(),
807 kind: "aws_access_key".into(),
808 path: "a.env".into(),
809 line: 1,
810 commit: "c".into(),
811 preview: "AKIA…".into(),
812 test_value: test_value.map(str::to_owned),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar813 pattern_id: None,
814 pattern_name: None,
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays815 }
816 }
817
818 #[test]
819 fn dismissed_secrets_and_test_values_go_through() {
820 let known = vec![
821 ("allowed".to_owned(), "sec_1".to_owned(), "allowed".to_owned()),
822 ("resolved".to_owned(), "sec_2".to_owned(), "resolved".to_owned()),
823 ("blocked".to_owned(), "sec_3".to_owned(), "blocked".to_owned()),
824 ];
825 let secrets = [secret("allowed", None), secret("resolved", None), secret("example", Some("it says it is an example")), secret("real", None)];
826 assert_eq!(let_through(&known, &secrets), ["allowed", "example"]);
827 }
828
829 #[test]
830 fn a_push_says_when_it_landed_unscanned() {
831 let pushed: Pushed = serde_json::from_value(serde_json::json!({
832 "repoId": "rep_1", "ref": "refs/heads/import", "after": "abc", "defaultBranch": false, "unscanned": true
833 }))
834 .unwrap();
835 assert!(pushed.unscanned && pushed.before.is_none() && pushed.git_ref == "refs/heads/import");
836 let ordinary: Pushed = serde_json::from_value(serde_json::json!({ "repoId": "rep_1", "ref": "refs/heads/main", "after": "abc", "defaultBranch": true })).unwrap();
837 assert!(!ordinary.unscanned);
838 }
839
840 #[test]
841 fn owners_are_told_what_landed_and_what_to_do() {
842 let one = history::landed_secrets_intro("acme", "rocket", "import", 1);
843 assert!(one.contains("A push to import in acme/rocket") && one.contains("a secret that looks real") && one.contains("Rotate"));
844 assert!(history::landed_secrets_intro("acme", "rocket", "main", 3).contains("3 secrets that look real"));
845 }
846
847 #[test]
848 fn dismissing_a_secret_takes_admin_and_a_dependency_write() {
Merge main (membership, two-factor, GitHub repo roles) into tokens849 assert_eq!(Security::capability_for("sec_1"), Capability::SecurityAlerts);
850 assert_eq!(Security::capability_for("vul_1"), Capability::SecurityAlerts);
Spend caps: a monthly budget for comped workspaces and a daily breaker on what g1t pays851 }
852}

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