Skip to content

g1t/services/security/src/lib.rs

807 lines37,164 bytesCodeBlame

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.

Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1//! The security service: what g1t finds wrong in a repository, and the
2//! upkeep that fixes it without a person.
3//!
4//! - Secrets. The repos service refuses pushes that add one
5//! (`push_blocked` says which were allowed and records the rest), and
6//! each repository's history is scanned once, in the background.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily7//! A push too large to scan first is let through, and its new commits
8//! are scanned after they land (`history::advance_push_scan`).
9//! - Alerts are open, dismissed (with a reason, a comment and who) or
10//! fixed, and each keeps an activity log. Likely test values are listed
11//! apart and never block a push or count as critical.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API12//! - Dependencies. On every push to a default branch, and daily, the
13//! lockfiles are read and every package checked against OSV. Each
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily14//! vulnerable package with a fix gets a security update: g1t itself
15//! (`User::system`) opens a pull request raising its version, made in a
16//! sandbox (the runner's `bump`), which lands through the branch's
g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent17//! required checks. Only when code has to change is g1t put on an
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily18//! issue for it.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API19//!
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//!
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API29//! Other services reach it over `POST /rpc/<method>`; see
30//! `g1t_contracts::security`.
31
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar32mod code_scanning;
33mod config;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API34mod deps;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar35mod fixes;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API36mod history;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar37mod manifests;
38mod overview;
39mod patterns;
40mod planning;
41mod pull_text;
42mod ranges;
43mod registries;
44mod schedule;
45mod secret_alerts;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily46mod security_updates;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API47mod store;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar48mod suite;
49mod suite_store;
50mod supply_chain;
51mod timezones;
52mod update_store;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily53mod updates;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar54mod version_updates;
55mod yaml;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API56
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look57use g1t_contracts::access::{self, Capability};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API58use g1t_contracts::events::{Event, WorkspaceRenamed};
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily59use g1t_contracts::security::UPDATE_BRANCH_PREFIX;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look60use g1t_contracts::repos::{GetArgs, PathByIdArgs, Repo, RepoPath, RepoStatus, StatusByIdArgs};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API61use g1t_contracts::security::*;
62use g1t_contracts::time::rfc3339;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily63use g1t_contracts::{FailureCode, Outcome, User};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API64use g1t_kit::{args, now_ms, reply, rpc_method};
65use serde::Deserialize;
66use worker::{Context, Env, Fetcher, MessageBatch, Request, Response, Result, ScheduleContext, ScheduledEvent, event};
67
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily68use store::{Activity, RepoRow, Store};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API69
70/// Repositories whose history is continued per sweep, and pages each.
71const HISTORIES_PER_SWEEP: u32 = 5;
72const PAGES_PER_SWEEP: u32 = 4;
73/// Repositories whose dependencies are read again per sweep.
74const DEPENDENCIES_PER_SWEEP: u32 = 10;
75const DAY_MS: u64 = 24 * 60 * 60 * 1000;
76const MAX_REASON_CHARS: usize = 500;
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA77/// What seeing and dismissing a repository's findings takes: the Write
78/// role, as on GitHub, or a security manager of its workspace. Changing its
79/// security settings takes Admin (`ManageSecurity`).
80const SEE_FINDINGS: Capability = Capability::SecurityAlerts;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API81
82pub struct Security {
83 store: Store,
84 identity: Fetcher,
85 repos: Fetcher,
86 work: Fetcher,
87 runner: Fetcher,
88 billing: Fetcher,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar89 actions: Fetcher,
90 /// The events service: security events for webhooks and the inbox,
91 /// and audit entries. Optional so a deployment without the binding
92 /// still scans.
93 events: Option<Fetcher>,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API94}
95
96fn fail<T>(code: FailureCode, message: impl Into<String>) -> Outcome<T> {
97 Outcome::fail(code, message)
98}
99
100/// `git.push`, as far as this service reads it.
101#[derive(Deserialize)]
102#[serde(rename_all = "camelCase")]
103struct Pushed {
104 repo_id: String,
105 #[serde(default)]
106 default_branch: bool,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily107 #[serde(default, rename = "ref")]
108 git_ref: String,
109 #[serde(default)]
110 before: Option<String>,
111 #[serde(default)]
112 after: String,
113 /// Too large to scan before it was stored.
114 #[serde(default)]
115 unscanned: bool,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API116}
117
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily118/// `pull.merged`, `pull.closed` and `checks.completed`, as far as this
119/// service reads them.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API120#[derive(Deserialize)]
121#[serde(rename_all = "camelCase")]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily122struct PullHappened {
123 repo_id: String,
124 number: u32,
125 #[serde(default)]
126 status: Option<String>,
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")]
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API136struct 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>,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API141}
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(),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API154 })
155 }
156
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look157 /// 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>> {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API167 let hidden = || fail(FailureCode::NotFound, "Repository not found.");
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look168 if viewer.is_none() {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API169 return Ok(hidden());
170 }
171 let repo: Outcome<Repo> =
172 g1t_kit::call(&self.repos, "get", &GetArgs { path: path.clone(), viewer: viewer.clone() }).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look173 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());
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API179 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look180 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))
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API189 }
190
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look191 /// Whether the repository is neither archived nor deleted. When repos
192 /// cannot say, it is taken as active.
193 async fn active(&self, repo_id: &str) -> Result<bool> {
194 let status: Result<RepoStatus> =
195 g1t_kit::call(&self.repos, "status_by_id", &StatusByIdArgs { id: repo_id.to_owned() }).await;
196 Ok(match status {
197 Ok(status) => status.active(),
198 Err(error) => {
199 worker::console_error!("security: status_by_id {repo_id}: {error}");
200 true
201 }
202 })
203 }
204
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API205 /// Records a repository named in an event, by id. Forks are not
206 /// recorded: a pull request's findings belong to its repository.
207 async fn register_by_id(&self, repo_id: &str) -> Result<Option<RepoRow>> {
208 if let Some(row) = self.store.repo(repo_id).await? {
209 return Ok(Some(row));
210 }
211 let path: Option<RepoPath> = g1t_kit::call(&self.repos, "path_by_id", &PathByIdArgs { id: repo_id.to_owned() }).await?;
212 match path {
213 Some(path) => Ok(Some(self.store.register(repo_id, &path.namespace, &path.name).await?)),
214 None => Ok(None),
215 }
216 }
217
218 async fn overview(&self, a: OverviewArgs) -> Result<Outcome<SecurityOverview>> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look219 let mut repo = match self.member_repo(&a.repo, &a.viewer, SEE_FINDINGS).await? {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API220 Outcome::Ok(repo) => repo,
221 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
222 };
223 // The first look at a repository reads its dependencies at once;
224 // its history is scanned in the background.
225 if repo.deps_scanned_at.is_none() {
226 self.scan_dependencies(&repo).await?;
227 repo = self.store.repo(&repo.repo_id).await?.unwrap_or(repo);
228 }
229 let (counts, _, _) = self.store.counts(&repo.repo_id).await?;
230 Ok(Outcome::Ok(SecurityOverview {
231 repo_id: repo.repo_id.clone(),
232 counts,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily233 secret_counts: self.store.secret_counts(&repo.repo_id).await?,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API234 secrets: self.store.secrets(&repo.repo_id).await?,
235 vulnerabilities: self.store.vulnerabilities(&repo.repo_id).await?,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily236 activity: self.store.activity(&repo.repo_id, ACTIVITY_SHOWN).await?,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API237 scan: repo.scan_state(),
238 upkeep: repo.upkeep != 0,
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar239 version_updates: self.version_updates_view(&repo).await?,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API240 }))
241 }
242
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA243 /// What dismissing or reopening alert `id` takes: Write, for a secret
244 /// as for a vulnerable dependency, as on GitHub; a security manager may
245 /// too.
246 fn capability_for(_id: &str) -> Capability {
247 SEE_FINDINGS
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily248 }
249
250 async fn dismiss(&self, a: DismissArgs) -> Result<Outcome<AlertChange>> {
251 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Self::capability_for(&a.id)).await? {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API252 Outcome::Ok(repo) => repo,
253 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
254 };
255 if !a.actor.verified {
256 return Ok(fail(FailureCode::Forbidden, "Confirm your email address first."));
257 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily258 let comment: String = a.comment.trim().chars().take(MAX_COMMENT_CHARS).collect();
259 let comment = (!comment.is_empty()).then_some(comment);
260 if let Some(finding) = self.store.secret(&repo.repo_id, &a.id).await? {
261 if !a.reason.for_secrets() {
262 return Ok(fail(
263 FailureCode::Invalid,
264 "A secret is dismissed as false_positive, used_in_tests, revoked or wont_fix.",
265 ));
266 }
267 if finding.state != AlertState::Open {
268 return Ok(fail(FailureCode::Conflict, "This alert is not open. Reopen it first to dismiss it again."));
269 }
270 self.store
271 .dismiss_secret(&repo.repo_id, &a.id, a.reason, &a.actor.username, comment.as_deref())
272 .await?;
273 self.store
274 .record(&repo.repo_id, &[Activity {
275 alert_id: &a.id,
276 action: "dismissed",
277 actor: Some(&a.actor.username),
278 reason: Some(a.reason),
279 comment: comment.as_deref(),
280 number: None,
281 }])
282 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar283 let secret = self.store.secret(&repo.repo_id, &a.id).await?;
284 if let Some(secret) = &secret {
285 // Revoked is fixed; any other reason, dismissed.
286 let action = if a.reason == DismissReason::Revoked { "fixed" } else { "dismissed" };
287 let event = g1t_contracts::security_suite::SecurityEvent {
288 reason: Some(a.reason.as_str().to_owned()),
289 ..secret_alerts::secret_event(&repo, secret)
290 };
291 self.alert_event(g1t_contracts::security_suite::AlertType::SecretScanning, action, &repo, event, Some(a.actor.id.clone()))
292 .await;
293 }
294 return Ok(Outcome::Ok(AlertChange { secret, vulnerability: None }));
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily295 }
296 let Some(vuln) = self.store.vulnerability(&repo.repo_id, &a.id).await? else {
297 return Ok(fail(FailureCode::NotFound, "No such alert."));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API298 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily299 if a.reason.for_secrets() {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API300 return Ok(fail(
301 FailureCode::Invalid,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily302 "A dependency is dismissed as fix_started, no_bandwidth, tolerable_risk, inaccurate or not_used.",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API303 ));
304 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily305 if vuln.state != AlertState::Open {
306 return Ok(fail(FailureCode::Conflict, "This alert is not open. Reopen it first to dismiss it again."));
307 }
308 self.store
309 .dismiss_vulnerability(&repo.repo_id, &a.id, a.reason, &a.actor.username, comment.as_deref())
310 .await?;
311 self.store
312 .record(&repo.repo_id, &[Activity {
313 alert_id: &a.id,
314 action: "dismissed",
315 actor: Some(&a.actor.username),
316 reason: Some(a.reason),
317 comment: comment.as_deref(),
318 number: None,
319 }])
320 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar321 let vulnerability = self.store.vulnerability(&repo.repo_id, &a.id).await?;
322 if let Some(vuln) = &vulnerability {
323 let event = g1t_contracts::security_suite::SecurityEvent { reason: Some(a.reason.as_str().to_owned()), ..deps::vulnerability_event(&repo, vuln) };
324 self.alert_event(g1t_contracts::security_suite::AlertType::Vulnerability, "dismissed", &repo, event, Some(a.actor.id.clone())).await;
325 }
326 Ok(Outcome::Ok(AlertChange { secret: None, vulnerability }))
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily327 }
328
329 async fn reopen(&self, a: ReopenArgs) -> Result<Outcome<AlertChange>> {
330 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Self::capability_for(&a.id)).await? {
331 Outcome::Ok(repo) => repo,
332 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API333 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily334 if !a.actor.verified {
335 return Ok(fail(FailureCode::Forbidden, "Confirm your email address first."));
336 }
337 let reopened = Activity { alert_id: &a.id, action: "reopened", actor: Some(&a.actor.username), reason: None, comment: None, number: None };
338 if let Some(finding) = self.store.secret(&repo.repo_id, &a.id).await? {
339 if finding.state == AlertState::Open {
340 return Ok(fail(FailureCode::Conflict, "This alert is already open."));
341 }
342 // A secret that never landed has nothing to reopen to but blocked.
343 let status = if finding.source == "push" && finding.test_value.is_none() { SecretStatus::Blocked } else { SecretStatus::Open };
344 self.store.reopen_secret(&repo.repo_id, &a.id, status).await?;
345 self.store.record(&repo.repo_id, &[reopened]).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar346 let secret = self.store.secret(&repo.repo_id, &a.id).await?;
347 if let Some(secret) = &secret {
348 self.alert_event(
349 g1t_contracts::security_suite::AlertType::SecretScanning,
350 "reopened",
351 &repo,
352 secret_alerts::secret_event(&repo, secret),
353 Some(a.actor.id.clone()),
354 )
355 .await;
356 }
357 return Ok(Outcome::Ok(AlertChange { secret, vulnerability: None }));
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily358 }
359 let Some(vuln) = self.store.vulnerability(&repo.repo_id, &a.id).await? else {
360 return Ok(fail(FailureCode::NotFound, "No such alert."));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API361 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily362 if vuln.state != AlertState::Dismissed {
363 return Ok(fail(FailureCode::Conflict, "Only a dismissed alert can be reopened; a fixed one reopens when it is found again."));
364 }
365 self.store.reopen_vulnerability(&repo.repo_id, &a.id).await?;
366 self.store.record(&repo.repo_id, &[reopened]).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar367 let vulnerability = self.store.vulnerability(&repo.repo_id, &a.id).await?;
368 if let Some(vuln) = &vulnerability {
369 self.alert_event(
370 g1t_contracts::security_suite::AlertType::Vulnerability,
371 "reopened",
372 &repo,
373 deps::vulnerability_event(&repo, vuln),
374 Some(a.actor.id.clone()),
375 )
376 .await;
377 }
378 Ok(Outcome::Ok(AlertChange { secret: None, vulnerability }))
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API379 }
380
381 async fn rescan(&self, a: RescanArgs) -> Result<Outcome<ScanState>> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look382 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), SEE_FINDINGS).await? {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API383 Outcome::Ok(repo) => repo,
384 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
385 };
386 self.store.restart_history(&repo.repo_id).await?;
387 self.scan_dependencies(&repo).await?;
388 if let Some(fresh) = self.store.repo(&repo.repo_id).await? {
389 self.advance_history(&fresh, 1).await?;
390 }
391 let state = self.store.repo(&repo.repo_id).await?.map(|row| row.scan_state()).unwrap_or_default();
392 Ok(Outcome::Ok(state))
393 }
394
395 async fn set_upkeep(&self, a: SetUpkeepArgs) -> Result<Outcome<bool>> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look396 // Whether agents keep its dependencies up to date is one of its
397 // settings.
398 let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Capability::ManageSettings).await? {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API399 Outcome::Ok(repo) => repo,
400 Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)),
401 };
402 if !a.actor.verified {
403 return Ok(fail(FailureCode::Forbidden, "Confirm your email address first."));
404 }
405 self.store.set_upkeep(&repo.repo_id, a.enabled, &a.actor.username).await?;
406 Ok(Outcome::Ok(a.enabled))
407 }
408
409 async fn workspace(&self, a: WorkspaceArgs) -> Result<Outcome<Vec<RepoSecurity>>> {
410 let workspace = a.workspace.to_lowercase();
411 if !a.viewer.as_ref().is_some_and(|user| user.is_member(&workspace)) {
412 return Ok(fail(FailureCode::NotFound, "Workspace not found."));
413 }
Usage, Billing settings and prepaid AI credit; fixes from the UX audit414 // Only the repositories whose findings the viewer may see. The
415 // Write role is never had through being public, so treating each
416 // as private changes nothing.
417 let repos: Vec<RepoRow> = self
418 .store
419 .in_namespace(&workspace)
420 .await?
421 .into_iter()
422 .filter(|repo| {
423 let target = access::RepoRef { id: &repo.repo_id, namespace: &workspace, private: true };
424 access::can(a.viewer.as_ref(), target, SEE_FINDINGS)
425 })
426 .collect();
427 // Every repository's counts at once, not one after another.
428 let counts = futures_util::future::try_join_all(repos.iter().map(|repo| self.store.counts(&repo.repo_id))).await?;
429 let list = repos
430 .into_iter()
431 .zip(counts)
432 .map(|(repo, (counts, secrets, vulnerabilities))| RepoSecurity {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API433 repo_id: repo.repo_id,
434 name: repo.name,
435 counts,
436 secrets,
437 vulnerabilities,
438 upkeep: repo.upkeep != 0,
439 dependencies_scanned_at: repo.deps_scanned_at,
Usage, Billing settings and prepaid AI credit; fixes from the UX audit440 })
441 .collect();
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API442 Ok(Outcome::Ok(list))
443 }
444
445 /// Push protection's question: which of these secrets were allowed?
446 /// The others are recorded as blocked, so someone can allow them.
447 async fn push_blocked(&self, a: PushBlockedArgs) -> Result<PushVerdict> {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar448 let repo = self.store.register(&a.repo_id, &a.path.namespace, &a.path.name).await?;
449 if let Some(private) = a.private {
450 self.store.set_private(&a.repo_id, private).await?;
451 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API452 let fingerprints: Vec<String> = a.secrets.iter().map(|secret| secret.fingerprint.clone()).collect();
453 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 bar454 let mut allowed = let_through(&known, &a.secrets);
455 // Bypassed with a reason: let through, whatever the alert says now.
456 allowed.extend(self.store.bypassed(&a.repo_id, &fingerprints).await?);
457 allowed.sort();
458 allowed.dedup();
459 let all = a.secrets.clone();
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API460 let fresh: Vec<NewSecret> = a
461 .secrets
462 .into_iter()
463 .filter(|secret| !known.iter().any(|(fingerprint, _, _)| *fingerprint == secret.fingerprint))
464 .collect();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily465 // A likely test value goes through, so it lands: open, not blocked.
466 let (tests, real): (Vec<NewSecret>, Vec<NewSecret>) = fresh.into_iter().partition(|secret| secret.test_value.is_some());
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API467 self.store
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily468 .add_secrets(&a.repo_id, &real, SecretStatus::Blocked, "push", a.pusher.as_deref())
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API469 .await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily470 self.store
471 .add_secrets(&a.repo_id, &tests, SecretStatus::Open, "push", a.pusher.as_deref())
472 .await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar473 let fresh: Vec<String> = real.iter().map(|secret| secret.fingerprint.clone()).collect();
474 self.secrets_found(&repo, &all, "push", &fresh, a.pusher.as_deref()).await?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API475 let ids = self
476 .store
477 .known(&a.repo_id, &fingerprints)
478 .await?
479 .into_iter()
480 .filter(|(fingerprint, _, _)| !allowed.contains(fingerprint))
481 .map(|(fingerprint, id, _)| (fingerprint, id))
482 .collect();
483 Ok(PushVerdict { allowed, ids })
484 }
485
486 /// What happens on the bus that concerns this service.
487 async fn on_event(&self, event: &Event) -> Result<()> {
488 match event.kind.as_str() {
489 "git.push" => {
490 let Ok(pushed) = serde_json::from_value::<Pushed>(event.data.clone()) else {
491 return Ok(());
492 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily493 // Too large to scan before it was stored: its new commits
494 // are scanned now, on whichever branch.
495 if pushed.unscanned
496 && !pushed.after.is_empty()
497 && let Some(repo) = self.register_by_id(&pushed.repo_id).await?
498 {
499 let id = self
500 .store
501 .add_push_scan(&repo.repo_id, &pushed.git_ref, &pushed.after, pushed.before.as_deref(), event.actor.as_deref())
502 .await?;
503 self.advance_push_scan(&id, history::PUSH_PAGES_AT_ONCE).await?;
504 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar505 // A version update's branch, or a grouped security update's,
506 // pushed by its sandbox: time for its pull request.
507 if !pushed.default_branch
508 && let Some(branch) = pushed.git_ref.strip_prefix("refs/heads/")
509 && self.update_pull_pushed(&pushed.repo_id, branch, &pushed.after).await?
510 {
511 return Ok(());
512 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily513 // A security update's branch, pushed by its sandbox: time for
514 // its pull request.
515 if let Some(branch) = pushed.git_ref.strip_prefix("refs/heads/")
516 && branch.starts_with(UPDATE_BRANCH_PREFIX)
517 {
518 self.update_pushed(&pushed.repo_id, branch).await?;
519 return Ok(());
520 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API521 if !pushed.default_branch {
522 return Ok(());
523 }
524 if let Some(repo) = self.register_by_id(&pushed.repo_id).await? {
525 self.scan_dependencies(&repo).await?;
526 if repo.history != "done" {
527 self.advance_history(&repo, 1).await?;
528 }
529 }
530 }
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily531 "pull.merged" | "pull.closed" | "checks.completed" => {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar532 if let Ok(happened) = serde_json::from_value::<PullHappened>(event.data.clone())
533 && !(event.kind != "checks.completed" && self.update_pull_closed(&event.kind, &happened.repo_id, happened.number).await?)
534 {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily535 self.update_pull_event(
536 &event.kind,
537 &happened.repo_id,
538 happened.number,
539 happened.status.as_deref(),
540 event.actor.as_deref(),
541 )
542 .await?;
543 }
544 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar545 "comment.created" => self.update_comment(event).await?,
546 // A pull request opened or its head moved: a dependency update
547 // file it changes is checked (not on `pull.ready`, which moves
548 // nothing), and dependency review runs.
549 "pull.opened" | "pull.updated" | "pull.ready" => {
550 if event.kind != "pull.ready" {
551 self.check_dependabot_file(event).await?;
552 }
553 if let Ok(happened) = serde_json::from_value::<PullHappened>(event.data.clone()) {
554 self.review_pull(&happened.repo_id, happened.number).await?;
555 }
556 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API557 "repo.created" => {
558 if let Ok(created) = serde_json::from_value::<Created>(event.data.clone()) {
Usage, Billing settings and prepaid AI credit; fixes from the UX audit559 // Recorded with its visibility, so the overview never takes a
560 // public repository for a private one.
561 if self.register_by_id(&created.repo_id).await?.is_some()
562 && let Some(private) = created.is_private
563 {
564 self.store.set_private(&created.repo_id, private).await?;
565 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API566 }
567 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar568 "repo.visibility_changed" => {
569 if let Ok(changed) = serde_json::from_value::<g1t_contracts::events::RepoVisibilityChanged>(event.data.clone())
570 && self.store.repo(&changed.repo_id).await?.is_some()
571 {
572 self.store.set_private(&changed.repo_id, changed.is_private).await?;
573 }
574 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look575 // A repository transferred or renamed: it is recorded under its new path.
576 "repo.transferred" | "repo.renamed" => {
577 if let Some(moved) = g1t_kit::transfer::read(event) {
578 // Where it is now, so moves heard out of order end in
579 // the same place.
580 let now: Option<g1t_contracts::repos::RepoPath> = g1t_kit::call(
581 &self.repos,
582 "path_by_id",
583 &g1t_contracts::repos::PathByIdArgs { id: moved.repo_id.clone() },
584 )
585 .await?;
586 let current = now.map_or_else(
587 || moved.destination().to_owned(),
588 |path| format!("{}/{}", path.namespace, path.name),
589 );
590 if let Some((namespace, name)) = current.split_once('/') {
591 self.store.moved(&moved.repo_id, namespace, name).await?;
592 }
593 }
594 }
595 // A repository purged: everything found in it goes.
596 "repo.purged" => {
597 if let Some(g1t_kit::lifecycle::Lifecycle::Purged(purged)) = g1t_kit::lifecycle::read(event) {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar598 self.store.purge_suite(&purged.repo_id).await?;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look599 self.store.purge(&purged.repo_id).await?;
600 }
601 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API602 "workspace.renamed" => {
603 if let Ok(renamed) = serde_json::from_value::<WorkspaceRenamed>(event.data.clone()) {
604 self.store.rename_namespace(&renamed.stale_slugs(&renamed.to), &renamed.to).await?;
605 }
606 }
607 _ => {}
608 }
609 Ok(())
610 }
611
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily612 /// The sweep: continues history scans and scans of pushes that landed
613 /// unscanned, catches security updates whose sandbox never pushed, and
614 /// reads dependencies that have not been read for a day.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API615 async fn sweep(&self) -> Result<()> {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily616 for scan in self.store.pending_push_scans(HISTORIES_PER_SWEEP).await? {
617 if let Err(error) = self.advance_push_scan(&scan.id, PAGES_PER_SWEEP).await {
618 worker::console_error!("security: push scan {} not continued: {error}", scan.id);
619 }
620 }
621 if let Err(error) = self.stalled_updates().await {
622 worker::console_error!("security: stalled security updates not handled: {error}");
623 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar624 if let Err(error) = self.sweep_suite().await {
625 worker::console_error!("security: daily snapshots and validity checks: {error}");
626 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look627 // Archived and deleted repositories wait; a few more are looked at
628 // so that they do not hold up the rest.
629 let mut histories = 0;
630 for repo in self.store.unfinished_histories(HISTORIES_PER_SWEEP * 4).await? {
631 if histories == HISTORIES_PER_SWEEP {
632 break;
633 }
634 if !self.active(&repo.repo_id).await? {
635 continue;
636 }
637 histories += 1;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API638 if let Err(error) = self.advance_history(&repo, PAGES_PER_SWEEP).await {
639 worker::console_error!("security: history of {} not scanned: {error}", repo.repo_id);
640 }
641 }
642 let day_ago = rfc3339(now_ms().saturating_sub(DAY_MS));
643 for repo in self.store.stale_dependencies(&day_ago, DEPENDENCIES_PER_SWEEP).await? {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look644 if !self.active(&repo.repo_id).await? {
645 self.store
646 .skip_dependencies(&repo.repo_id, "Dependencies are not checked while the repository is archived or deleted.")
647 .await?;
648 continue;
649 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API650 if let Err(error) = self.scan_dependencies(&repo).await {
651 worker::console_error!("security: dependencies of {} not read: {error}", repo.repo_id);
652 }
653 }
654 Ok(())
655 }
656}
657
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily658/// The fingerprints a push may carry: those someone dismissed (allowed),
659/// and likely test values, which are recorded but never stop a push.
660/// `known` is (fingerprint, id, status) of findings already recorded.
661fn let_through(known: &[(String, String, String)], secrets: &[NewSecret]) -> Vec<String> {
662 let mut allowed: Vec<String> = known
663 .iter()
664 .filter(|(_, _, status)| status == "allowed")
665 .map(|(fingerprint, _, _)| fingerprint.clone())
666 .chain(secrets.iter().filter(|secret| secret.test_value.is_some()).map(|secret| secret.fingerprint.clone()))
667 .collect();
668 allowed.sort();
669 allowed.dedup();
670 allowed
671}
672
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API673#[event(fetch)]
674async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> {
675 let Some(method) = rpc_method(&request) else {
676 return Response::error("Not found", 404);
677 };
678 let security = Security::new(&env)?;
679 let body: serde_json::Value = request.json().await?;
680 match method.as_str() {
681 "overview" => reply(&security.overview(args(body)?).await?),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily682 "dismiss" => reply(&security.dismiss(args(body)?).await?),
683 "reopen" => reply(&security.reopen(args(body)?).await?),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API684 "rescan" => reply(&security.rescan(args(body)?).await?),
685 "set_upkeep" => reply(&security.set_upkeep(args(body)?).await?),
686 "workspace" => reply(&security.workspace(args(body)?).await?),
687 "push_blocked" => reply(&security.push_blocked(args(body)?).await?),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar688 "check_updates" => reply(&security.check_updates(args(body)?).await?),
689 // The security suite.
690 "patterns_for" => reply(&security.patterns_for(args(body)?).await?),
691 "custom_patterns" => reply(&security.custom_patterns(args(body)?).await?),
692 "save_custom_pattern" => reply(&security.save_custom_pattern(args(body)?).await?),
693 "delete_custom_pattern" => reply(&security.delete_custom_pattern(args(body)?).await?),
694 "dry_run_pattern" => reply(&security.dry_run_pattern(args(body)?).await?),
695 "secret_alert" => reply(&security.secret_alert(args(body)?).await?),
696 "bypass" => reply(&security.bypass(args(body)?).await?),
697 "bypass_requests" => reply(&security.bypass_requests(args(body)?).await?),
698 "review_bypass" => reply(&security.review_bypass(args(body)?).await?),
699 "check_validity" => reply(&security.check_validity(args(body)?).await?),
700 "upload_sarif" => reply(&security.upload_sarif(args(body)?).await?),
701 "sarif_status" => reply(&security.sarif_status(args(body)?).await?),
702 "code_scanning" => reply(&security.code_scanning(args(body)?).await?),
703 "code_alert" => reply(&security.code_alert(args(body)?).await?),
704 "set_code_alert_state" => reply(&security.set_code_alert_state(args(body)?).await?),
705 "pull_code_scanning" => reply(&security.pull_code_scanning(args(body)?).await?),
706 "fix_alert" => reply(&security.fix_alert(args(body)?).await?),
707 "dependency_graph" => reply(&security.dependency_graph(args(body)?).await?),
708 "sbom" => reply(&security.sbom(args(body)?).await?),
709 "dependency_review" => reply(&security.dependency_review(args(body)?).await?),
710 "security_settings" => reply(&security.security_settings(args(body)?).await?),
711 "set_security_settings" => reply(&security.set_security_settings(args(body)?).await?),
712 "workspace_security_settings" => reply(&security.workspace_security_settings(args(body)?).await?),
713 "set_workspace_security_settings" => reply(&security.set_workspace_security_settings(args(body)?).await?),
714 "security_overview" => reply(&security.security_overview(args(body)?).await?),
715 "workspace_alerts" => reply(&security.workspace_alerts(args(body)?).await?),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API716 _ => Response::error("Unknown method", 404),
717 }
718}
719
720#[event(queue)]
721async fn queue(batch: MessageBatch<Event>, env: Env, _ctx: Context) -> Result<()> {
722 let security = Security::new(&env)?;
723 for message in batch.messages()? {
724 let event = message.body();
725 if let Err(error) = security.on_event(event).await {
726 // Scans are idempotent and the sweep catches up, so one failed
727 // event is logged rather than retried.
728 worker::console_error!("security: {} {} failed: {error}", event.kind, event.id);
729 }
730 }
731 Ok(())
732}
733
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar734/// The cron (wrangler.jsonc) that runs version updates that are due.
735const VERSION_UPDATES_CRON: &str = "*/5 * * * *";
736
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API737#[event(scheduled)]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar738async fn scheduled(event: ScheduledEvent, env: Env, _ctx: ScheduleContext) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API739 match Security::new(&env) {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar740 // Version updates run every few minutes, so a schedule's time is kept.
741 Ok(security) if event.cron() == VERSION_UPDATES_CRON => {
742 if let Err(error) = security.version_update_sweep().await {
743 worker::console_error!("security: the version update sweep failed: {error}");
744 }
745 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API746 Ok(security) => {
747 if let Err(error) = security.sweep().await {
748 worker::console_error!("security: the sweep failed: {error}");
749 }
750 }
751 Err(error) => worker::console_error!("security: could not start: {error}"),
752 }
753}
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily754
755#[cfg(test)]
756mod tests {
757 use super::*;
758
759 fn secret(fingerprint: &str, test_value: Option<&str>) -> NewSecret {
760 NewSecret {
761 fingerprint: fingerprint.into(),
762 kind: "aws_access_key".into(),
763 path: "a.env".into(),
764 line: 1,
765 commit: "c".into(),
766 preview: "AKIA…".into(),
767 test_value: test_value.map(str::to_owned),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar768 pattern_id: None,
769 pattern_name: None,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily770 }
771 }
772
773 #[test]
774 fn dismissed_secrets_and_test_values_go_through() {
775 let known = vec![
776 ("allowed".to_owned(), "sec_1".to_owned(), "allowed".to_owned()),
777 ("resolved".to_owned(), "sec_2".to_owned(), "resolved".to_owned()),
778 ("blocked".to_owned(), "sec_3".to_owned(), "blocked".to_owned()),
779 ];
780 let secrets = [secret("allowed", None), secret("resolved", None), secret("example", Some("it says it is an example")), secret("real", None)];
781 assert_eq!(let_through(&known, &secrets), ["allowed", "example"]);
782 }
783
784 #[test]
785 fn a_push_says_when_it_landed_unscanned() {
786 let pushed: Pushed = serde_json::from_value(serde_json::json!({
787 "repoId": "rep_1", "ref": "refs/heads/import", "after": "abc", "defaultBranch": false, "unscanned": true
788 }))
789 .unwrap();
790 assert!(pushed.unscanned && pushed.before.is_none() && pushed.git_ref == "refs/heads/import");
791 let ordinary: Pushed = serde_json::from_value(serde_json::json!({ "repoId": "rep_1", "ref": "refs/heads/main", "after": "abc", "defaultBranch": true })).unwrap();
792 assert!(!ordinary.unscanned);
793 }
794
795 #[test]
796 fn owners_are_told_what_landed_and_what_to_do() {
797 let one = history::landed_secrets_intro("acme", "rocket", "import", 1);
798 assert!(one.contains("A push to import in acme/rocket") && one.contains("a secret that looks real") && one.contains("Rotate"));
799 assert!(history::landed_secrets_intro("acme", "rocket", "main", 3).contains("3 secrets that look real"));
800 }
801
802 #[test]
803 fn dismissing_a_secret_takes_admin_and_a_dependency_write() {
Merge membership: owners, org roles, GitHub's repo roles, privileges, 2FA804 assert_eq!(Security::capability_for("sec_1"), Capability::SecurityAlerts);
805 assert_eq!(Security::capability_for("vul_1"), Capability::SecurityAlerts);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily806 }
807}

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