Skip to content
1,271 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.

Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1//! Looking for secrets in git: in what a push adds, before it is stored
2//! (push protection), and in a repository's history, a page at a time, for
3//! the security service. Also finds the lockfiles it reads dependencies
4//! from. What counts as a secret is `g1t_scan`'s business.
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look5//!
6//! Push protection also keeps a person's private address out of what they
7//! push, when they asked g1t to (see [`exposed_address`]).
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar8//!
9//! Custom patterns (the security suite's) are looked for alongside the
10//! built-in formats, in pushes, history and files committed through g1t
11//! itself; the security service says which apply ([`Repos::patterns_for`]).
12//! It can also run a pattern over the default branch for a dry run
13//! ([`Repos::match_pattern`]), and ask a landed secret's issuer whether it
14//! still works ([`Repos::check_secret`]), without the value ever leaving
15//! this service except to that issuer.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API16
17use std::cell::Cell;
18use std::collections::{HashSet, VecDeque};
19
20use futures_util::future::try_join_all;
21use g1t_contracts::User;
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look22use g1t_contracts::accounts::{CommitIdentityArgs, PushEmailGuard, mask_email};
Mission control shows where you are needed and what agents landed without you; git answers in about 200ms23use g1t_contracts::repos::{EntryKind, Repo, RepoPath};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API24use g1t_contracts::security::{
25 FindLockfilesArgs, HistoryPage, LockfileText, Lockfiles, NewSecret, PushBlockedArgs, PushVerdict,
26 ScanHistoryArgs,
27};
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar28use g1t_contracts::security_suite::{CheckSecretArgs, MatchPatternArgs, PatternMatch, PatternMatches, PatternSpec, PatternsForArgs, SecretValidity};
29use g1t_scan::custom::{self, Compiled};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API30use g1t_scan::lockfiles::Lockfile;
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer31use g1t_scan::pack::{ObjectKind, Pack, TreeItem, encode_tree};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API32use g1t_scan::protection::{self, Blocked};
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer33use worker::Result;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API34
35use crate::registry::store_key;
36use crate::store::{GitRepo, GitStore};
37
38/// Where people allow a secret: the project's Security page.
39const SITE: &str = "https://g1t.sh";
40/// A push adding more commits than this is scanned for this many of them.
41const MAX_PUSH_COMMITS: usize = 300;
42/// Files compared per commit, at most.
43const MAX_FILES_PER_COMMIT: usize = 300;
Merge main into Artifacts Phase 244/// Bases fetched from the store for a thin pack, at most, in all rounds
45/// together. Each is one or two store reads, each with a Cache API look.
46const MAX_BASES: usize = 200;
47/// Bases asked for at a time (two reads each when the pack does not say
48/// whether a base is a blob or a tree).
49const BASES_AT_ONCE: usize = 16;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily50/// The largest push that is read whole and scanned. A larger one is
51/// declined, since it cannot be checked (git_http.rs `LargePushes`).
52pub const MAX_SCANNED_PUSH: usize = 24 * 1024 * 1024;
53/// What marks an error as a push too large to scan.
54const UNSCANNABLE: &str = "push-unscannable:";
55
56/// Whether an error says the push was too large to scan.
57pub fn unscannable(error: &worker::Error) -> bool {
58 error.to_string().contains(UNSCANNABLE)
59}
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API60const READS_AT_ONCE: usize = 16;
61/// Directories never searched for lockfiles.
62const SKIPPED_DIRECTORIES: [&str; 8] = ["node_modules", "vendor", "target", ".git", "dist", "build", "third_party", ".venv"];
63const MAX_LOCKFILES: usize = 40;
64const MAX_LOCKFILE_DEPTH: usize = 4;
65const MAX_LOCKFILE_BYTES: usize = 16 * 1024 * 1024;
66
67fn mode(kind: EntryKind) -> &'static str {
68 match kind {
69 EntryKind::Tree => "40000",
70 EntryKind::Blob => "100644",
71 EntryKind::Exec => "100755",
72 EntryKind::Symlink => "120000",
73 EntryKind::Gitlink => "160000",
74 }
75}
76
77/// Objects for a walk: the pushed pack's first, then the repository's.
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge78pub(crate) struct Objects<'a, R: GitRepo> {
79 pub(crate) pack: &'a Pack,
80 pub(crate) repo: &'a R,
81 pub(crate) reads: Cell<u32>,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API82}
83
84impl<R: GitRepo> Objects<'_, R> {
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge85 pub(crate) async fn tree(&self, id: &str) -> Result<Vec<TreeItem>> {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API86 if let Some(items) = self.pack.tree(id) {
87 return Ok(items);
88 }
89 self.reads.set(self.reads.get() + 1);
90 Ok(self
91 .repo
92 .read_tree(id)
93 .await?
94 .unwrap_or_default()
95 .into_iter()
96 .map(|entry| TreeItem { mode: mode(entry.kind).to_owned(), name: entry.name, id: entry.hash })
97 .collect())
98 }
99
100 async fn blob(&self, id: &str) -> Result<Option<Vec<u8>>> {
101 if let Some(bytes) = self.pack.blob(id) {
102 return Ok(Some(bytes.to_vec()));
103 }
104 self.reads.set(self.reads.get() + 1);
105 self.repo.read_blob(id).await
106 }
107
Merge rulesets: branch and tag rules, agent-first, enforced on push and merge108 pub(crate) async fn commit_tree(&self, id: &str) -> Result<Option<String>> {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API109 if let Some(commit) = self.pack.commit(id) {
110 return Ok(Some(commit.tree));
111 }
112 self.reads.set(self.reads.get() + 1);
113 Ok(self.repo.log(id, 1).await?.into_iter().next().map(|commit| commit.tree_hash))
114 }
115}
116
117/// A file that differs between two trees: its path, the blob it was and
118/// the blob it is.
119struct Change {
120 path: String,
121 old: Option<String>,
122 new: String,
123}
124
125/// The regular files whose content differs between two trees. Each level
126/// is read at once; identical subtrees are skipped by id.
127async fn changed_files<R: GitRepo>(objects: &Objects<'_, R>, old_root: Option<String>, new_root: String) -> Result<Vec<Change>> {
128 let mut changes = Vec::new();
129 let mut level = vec![(String::new(), old_root, new_root)];
130 while !level.is_empty() && changes.len() < MAX_FILES_PER_COMMIT {
131 let read = try_join_all(level.iter().map(|(_, old, new)| async move {
132 let old = match old {
133 Some(old) => objects.tree(old).await?,
134 None => Vec::new(),
135 };
136 Ok::<_, worker::Error>((old, objects.tree(new).await?))
137 }))
138 .await?;
139 let mut next = Vec::new();
140 for ((prefix, _, _), (old, new)) in level.iter().zip(read) {
141 for item in &new {
142 let before = old.iter().find(|entry| entry.name == item.name);
143 if before.is_some_and(|before| before.id == item.id) {
144 continue;
145 }
146 let path = format!("{prefix}{}", item.name);
147 if item.is_tree() {
148 next.push((format!("{path}/"), before.filter(|b| b.is_tree()).map(|b| b.id.clone()), item.id.clone()));
149 } else if item.is_file() && changes.len() < MAX_FILES_PER_COMMIT {
150 changes.push(Change {
151 path,
152 old: before.filter(|b| b.is_file()).map(|b| b.id.clone()),
153 new: item.id.clone(),
154 });
155 }
156 }
157 }
158 level = next;
159 }
160 Ok(changes)
161}
162
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar163/// The scanner's custom patterns, compiled; any that no longer compile are
164/// skipped.
165pub fn compiled(patterns: &[PatternSpec]) -> Vec<Compiled> {
166 let specs: Vec<custom::PatternSpec> = patterns
167 .iter()
168 .map(|spec| custom::PatternSpec {
169 id: spec.id.clone(),
170 name: spec.name.clone(),
171 pattern: spec.pattern.clone(),
172 before: spec.before.clone(),
173 after: spec.after.clone(),
174 })
175 .collect();
176 custom::compile_all(&specs)
177}
178
179/// What a custom pattern found, as the security service records it.
180fn custom_secret(hit: custom::CustomHit, path: &str, commit: &str) -> NewSecret {
181 NewSecret {
182 fingerprint: hit.fingerprint(),
183 kind: custom::KIND.to_owned(),
184 path: path.to_owned(),
185 line: hit.line,
186 commit: commit.to_owned(),
187 preview: hit.preview(),
188 test_value: None,
189 pattern_id: Some(hit.pattern_id),
190 pattern_name: Some(hit.pattern_name),
191 }
192}
193
194/// How a sentence names a secret found: its format, or its pattern.
195pub fn secret_label(secret: &NewSecret) -> Option<String> {
196 if secret.kind == custom::KIND {
197 return Some(custom::label(secret.pattern_name.as_deref().unwrap_or("custom")));
198 }
199 g1t_scan::secrets::SecretKind::parse(&secret.kind).map(|kind| kind.label().to_owned())
200}
201
202/// The secrets a new file holds, built-in and custom, for a commit made
203/// through g1t rather than pushed.
204pub fn scan_file(path: &str, bytes: &[u8], commit: &str, patterns: &[Compiled]) -> Vec<NewSecret> {
205 let mut found: Vec<NewSecret> = protection::scan_change(path, None, bytes)
206 .into_iter()
207 .map(|hit| NewSecret {
208 fingerprint: hit.fingerprint(),
209 kind: hit.kind.id().to_owned(),
210 path: path.to_owned(),
211 line: hit.line,
212 commit: commit.to_owned(),
213 preview: hit.preview(),
214 test_value: hit.test_value().map(str::to_owned),
215 pattern_id: None,
216 pattern_name: None,
217 })
218 .collect();
219 found.extend(protection::scan_change_custom(path, None, bytes, patterns).into_iter().map(|hit| custom_secret(hit, path, commit)));
220 found
221}
222
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API223/// The secrets each change adds, found `READS_AT_ONCE` files at a time.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar224async fn scan_changes<R: GitRepo>(objects: &Objects<'_, R>, commit: &str, changes: Vec<Change>, patterns: &[Compiled]) -> Result<Vec<NewSecret>> {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API225 let mut found = Vec::new();
226 let changes: Vec<Change> = changes
227 .into_iter()
228 .filter(|change| !g1t_scan::secrets::skipped_path(&change.path))
229 .collect();
230 for batch in changes.chunks(READS_AT_ONCE) {
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer231 // A change's old and new contents at once: the new is nearly always
232 // in the pack, the old in the repository.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API233 let read = try_join_all(batch.iter().map(|change| async move {
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer234 let old = async {
235 match &change.old {
236 Some(old) => objects.blob(old).await,
237 None => Ok(None),
238 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API239 };
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer240 let (new, old) = futures_util::future::try_join(objects.blob(&change.new), old).await?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API241 Ok::<_, worker::Error>((new, old))
242 }))
243 .await?;
244 for (change, (new, old)) in batch.iter().zip(read) {
245 let Some(new) = new else { continue };
246 for hit in protection::scan_change(&change.path, old.as_deref(), &new) {
247 found.push(NewSecret {
248 fingerprint: hit.fingerprint(),
249 kind: hit.kind.id().to_owned(),
250 path: change.path.clone(),
251 line: hit.line,
252 commit: commit.to_owned(),
253 preview: hit.preview(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily254 test_value: hit.test_value().map(str::to_owned),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar255 pattern_id: None,
256 pattern_name: None,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API257 });
258 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar259 for hit in protection::scan_change_custom(&change.path, old.as_deref(), &new, patterns) {
260 found.push(custom_secret(hit, &change.path, commit));
261 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API262 }
263 }
264 Ok(found)
265}
266
Merge main into Artifacts Phase 2267/// What [`supply_bases`] did for a pack.
268#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
269pub struct Bases {
270 /// Bases the pack's deltas needed from outside it when it arrived: 0
271 /// for a pack sent whole, as g1t asks for (`no-thin`, git_http.rs).
272 pub missing: usize,
273 /// Bases asked of the store, in every round.
274 pub asked: usize,
275 /// Bases still missing at the end: objects that stay unresolved.
276 pub left: usize,
277}
278
279impl Bases {
280 /// Whether the pack came thin, its deltas based on objects outside it.
281 pub fn thin(&self) -> bool {
282 self.missing > 0
283 }
284}
285
286/// Fetches what a thin pack's deltas are based on from the repository.
287/// A base the pack's own trees name is read as what they say it is; any
288/// other is asked for as a blob and as a tree together, and whichever it
289/// is answers.
290///
291/// g1t asks for packs without outside bases (`no-thin`), so this is for
292/// clients that send thin ones anyway, and it is bounded: [`MAX_BASES`] in
293/// all, over up to three rounds for delta chains, [`BASES_AT_ONCE`] at a
294/// time. Asking for hundreds at once made a large thin push fan out into
295/// as many store reads, and Cache API looks beside them, all in flight
296/// together; the reads that failed were tried again and counted against
297/// the store's breaker (store.rs `invoke`), which then turned every read
298/// after them away, so the push was answered 503. A store that says it is
299/// busy stops the reading here. Bases left missing leave their objects
300/// unresolved, and the checks go on without them, as for any pack the
301/// store cannot complete.
302pub(crate) async fn supply_bases<R: GitRepo>(pack: &mut Pack, repo: &R) -> Result<Bases> {
303 let mut bases = Bases { missing: pack.missing_bases().len(), ..Bases::default() };
304 let mut busy = false;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API305 for _ in 0..3 {
306 let missing = pack.missing_bases();
Merge main into Artifacts Phase 2307 if missing.is_empty() || busy || bases.asked >= MAX_BASES {
308 break;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API309 }
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer310 let named = pack.named_kinds();
Merge main into Artifacts Phase 2311 // A read that fails is a base not found, unless the store is busy,
312 // which ends the reading.
313 let settle = |read: Result<Option<(ObjectKind, Vec<u8>)>>| match read {
314 Ok(found) => Ok(found),
315 Err(error) if crate::resilience::busy(&error.to_string()).is_some() => Err(()),
316 Err(_) => Ok(None),
317 };
318 let blob = async |id: &str| settle(repo.read_blob(id).await.map(|found| found.map(|bytes| (ObjectKind::Blob, bytes))));
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer319 let tree = async |id: &str| {
Merge main into Artifacts Phase 2320 settle(repo.read_tree(id).await.map(|found| {
321 found.map(|entries| {
322 let items: Vec<TreeItem> = entries
323 .into_iter()
324 .map(|entry| TreeItem { mode: mode(entry.kind).to_owned(), name: entry.name, id: entry.hash })
325 .collect();
326 (ObjectKind::Tree, encode_tree(&items))
327 })
328 }))
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer329 };
Merge main into Artifacts Phase 2330 let wanted: Vec<&String> = missing.iter().take(MAX_BASES - bases.asked).collect();
331 let mut progress = false;
332 for batch in wanted.chunks(BASES_AT_ONCE) {
333 let found = futures_util::future::join_all(batch.iter().map(|id| async {
334 match named.get(id.as_str()) {
335 Some(ObjectKind::Tree) => tree(id).await,
336 Some(_) => blob(id).await,
337 None => {
338 let (as_blob, as_tree) = futures_util::future::join(blob(id), tree(id)).await;
339 match (as_blob, as_tree) {
340 (Ok(Some(found)), _) | (_, Ok(Some(found))) => Ok(Some(found)),
341 (Err(()), _) | (_, Err(())) => Err(()),
342 _ => Ok(None),
343 }
344 }
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer345 }
Merge main into Artifacts Phase 2346 }))
347 .await;
348 bases.asked += batch.len();
349 for (id, object) in batch.iter().zip(found) {
350 match object {
351 Ok(Some((kind, data))) => {
352 pack.supply(id, kind, data);
353 progress = true;
354 }
355 Ok(None) => {}
356 Err(()) => busy = true,
357 }
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer358 }
Merge main into Artifacts Phase 2359 if busy {
360 break;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API361 }
362 }
363 if !progress {
Merge main into Artifacts Phase 2364 break;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API365 }
366 }
Merge main into Artifacts Phase 2367 bases.left = pack.missing_bases().len();
368 Ok(bases)
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API369}
370
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily371/// The secrets the commits in a push add, each secret once. A push too
372/// large to read is an error ([`unscannable`]): it is declined, never let
373/// through unread. A pack that cannot be read for another reason is let
374/// through, and said so in the logs; the store will judge it.
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer375#[cfg(test)]
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar376pub async fn scan_push<R: GitRepo>(repo: &R, body: &[u8], patterns: &[Compiled]) -> Result<Vec<NewSecret>> {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API377 if body.len() > MAX_SCANNED_PUSH {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily378 return Err(worker::Error::RustError(format!("{UNSCANNABLE} {} bytes", body.len())));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API379 }
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer380 let mut pack = crate::push_checks::read_pack(body);
381 if let Ok(pack) = &mut pack {
382 supply_bases(pack, repo).await?;
383 }
384 scan_pack(repo, body.len(), &pack, patterns).await
385}
386
387/// [`scan_push`] for a push of `size` bytes whose pack was read already,
388/// with its bases supplied (push_checks.rs).
389pub(crate) async fn scan_pack<R: GitRepo>(repo: &R, size: usize, pack: &std::result::Result<Pack, String>, patterns: &[Compiled]) -> Result<Vec<NewSecret>> {
390 if size > MAX_SCANNED_PUSH {
391 return Err(worker::Error::RustError(format!("{UNSCANNABLE} {size} bytes")));
392 }
393 let pack = match pack {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API394 Ok(pack) => pack,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily395 Err(problem) if problem.contains("too large") => {
396 return Err(worker::Error::RustError(format!("{UNSCANNABLE} {problem}")));
397 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API398 Err(problem) => {
399 worker::console_error!("push not scanned for secrets: {problem}");
400 return Ok(Vec::new());
401 }
402 };
403 if pack.unresolved() > 0 {
404 worker::console_error!("{} objects of a push could not be resolved for scanning", pack.unresolved());
405 }
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer406 let objects = Objects { pack, repo, reads: Cell::new(0) };
407 let objects = &objects;
408 let commits: Vec<(String, g1t_scan::pack::CommitInfo)> = pack
409 .commits()
410 .iter()
411 .take(MAX_PUSH_COMMITS)
412 .filter_map(|id| Some((id.clone(), pack.commit(id)?)))
413 .collect();
414 // The trees of the parents the pack does not hold, all read up front:
415 // usually the one commit the push builds on.
416 let mut outside: Vec<&String> = commits
417 .iter()
418 .filter_map(|(_, commit)| commit.parents.first())
419 .filter(|parent| !pack.contains(parent))
420 .collect();
421 outside.sort();
422 outside.dedup();
423 let outside_trees: std::collections::HashMap<&String, Option<String>> = outside
424 .iter()
425 .copied()
426 .zip(try_join_all(outside.iter().map(|parent| objects.commit_tree(parent))).await?)
427 .collect();
428 let outside_trees = &outside_trees;
429 // What each commit changes, all read at once.
430 let changed = try_join_all(commits.iter().map(|(_, commit)| async move {
431 let old_tree = match commit.parents.first() {
432 Some(parent) => match outside_trees.get(parent) {
433 Some(tree) => tree.clone(),
434 None => objects.commit_tree(parent).await?,
435 },
436 None => None,
437 };
438 changed_files(objects, old_tree, commit.tree.clone()).await
439 }))
440 .await?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API441 let mut found = Vec::new();
442 let mut seen_blobs = HashSet::new();
443 let mut seen_secrets = HashSet::new();
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer444 for ((id, _), changes) in commits.iter().zip(changed) {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API445 // Only content the push brings is new; a blob the repository has
446 // was looked at when it arrived.
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer447 let changes: Vec<Change> = changes
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API448 .into_iter()
449 .filter(|change| pack.contains(&change.new) && seen_blobs.insert((change.path.clone(), change.new.clone())))
450 .collect();
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer451 for secret in scan_changes(objects, id, changes, patterns).await? {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API452 if seen_secrets.insert(secret.fingerprint.clone()) {
453 found.push(secret);
454 }
455 }
456 }
457 Ok(found)
458}
459
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look460/// A commit in a push that would publish one of the pusher's own
461/// addresses while they keep it private: its id and the address. Only the
462/// commits the push adds are read; anyone else's address is no concern
463/// here. A pack that cannot be read is let through.
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer464#[cfg(test)]
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look465pub fn exposed_address(body: &[u8], guard: &PushEmailGuard) -> Option<(String, String)> {
466 if body.len() > MAX_SCANNED_PUSH {
467 return None;
468 }
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer469 exposed_in(&Pack::parse(&body[g1t_scan::pack::pack_start(body)?..]).ok()?, guard)
470}
471
472/// [`exposed_address`] for a pack read already.
473pub(crate) fn exposed_in(pack: &Pack, guard: &PushEmailGuard) -> Option<(String, String)> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look474 pack.commits().iter().find_map(|id| {
475 let commit = pack.commit(id)?;
476 [commit.author_email, commit.committer_email]
477 .into_iter()
478 .flatten()
479 .find(|email| guard.exposes(email))
480 .map(|email| (id.clone(), email))
481 })
482}
483
484/// What git shows a person whose push would publish their private address.
485pub fn exposed_message(commit: &str, email: &str, noreply: &str) -> Vec<String> {
486 let short: String = commit.chars().take(7).collect();
487 vec![
488 format!(
489 "push declined: commit {short} would publish {} while your email is private.",
490 mask_email(&email.to_lowercase())
491 ),
492 format!("Commit with {noreply} (git config user.email {noreply}) and amend,"),
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas493 format!("or change this in {}/settings/emails.", SITE.trim_start_matches("https://")),
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look494 ]
495}
496
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API497impl<S: GitStore> crate::Repos<S> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look498 /// What a push by `pusher` must not publish: their own addresses, when
499 /// they keep them private and block such pushes. An agent's push is
500 /// its person's. `None` when nothing is guarded, or identity cannot say.
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer501 pub(crate) async fn push_email_guard(&self, pusher: Option<&User>) -> Option<PushEmailGuard> {
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look502 let pusher = pusher?;
503 let person = pusher.acting.as_ref().map_or(pusher.id.clone(), |acting| acting.on_behalf_of.id.clone());
504 let identity = self.identity.as_ref()?;
505 g1t_kit::call::<_, Option<PushEmailGuard>>(identity, "push_email_guard", &CommitIdentityArgs { user_id: person })
506 .await
507 .unwrap_or_else(|error| {
508 worker::console_error!("push_email_guard failed: {error}");
509 None
510 })
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar511 }
512
513 /// The custom patterns the security service says `repo` is scanned
514 /// with; none when it cannot say.
515 pub(crate) async fn patterns_for(&self, repo: &Repo) -> Vec<PatternSpec> {
516 let Some(security) = &self.security else { return Vec::new() };
517 g1t_kit::call(
518 security,
519 "patterns_for",
520 &PatternsForArgs { repo_id: repo.id.clone(), namespace: repo.namespace.clone(), private: Some(repo.is_private) },
521 )
522 .await
523 .unwrap_or_else(|error| {
524 worker::console_error!("patterns_for failed: {error}");
525 Vec::new()
526 })
527 }
528
529 /// Of `found` in a change to `owner`, the secrets nobody let through:
530 /// the security service records them all and says which were allowed.
531 pub(crate) async fn blocked(&self, owner: &Repo, pusher: Option<&User>, found: Vec<NewSecret>) -> Vec<Blocked> {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API532 let owner_path = RepoPath { namespace: owner.namespace.clone(), name: owner.name.clone() };
533 let verdict = match &self.security {
534 Some(security) => g1t_kit::call::<_, PushVerdict>(
535 security,
536 "push_blocked",
537 &PushBlockedArgs {
538 repo_id: owner.id.clone(),
539 path: owner_path.clone(),
540 pusher: pusher.map(|user| user.username.clone()),
541 secrets: found.clone(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar542 private: Some(owner.is_private),
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API543 },
544 )
545 .await
546 .unwrap_or_else(|error| {
547 worker::console_error!("push_blocked failed: {error}");
548 PushVerdict::default()
549 }),
550 None => PushVerdict::default(),
551 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar552 found
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API553 .iter()
554 .filter(|secret| !verdict.allowed.contains(&secret.fingerprint))
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily555 // A likely test value is recorded, never a reason to refuse.
556 .filter(|secret| secret.test_value.is_none())
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API557 .filter_map(|secret| {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar558 let label = secret_label(secret)?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API559 let id = verdict.ids.iter().find(|(fingerprint, _)| *fingerprint == secret.fingerprint);
560 Some(Blocked {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar561 label,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API562 path: secret.path.clone(),
563 line: secret.line,
564 commit: secret.commit.clone(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar565 // Where it can be bypassed with a reason, or allowed.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API566 allow_url: id.map(|(_, id)| {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar567 format!("{SITE}/{}/{}/security/secret-scanning/{id}", owner_path.namespace, owner_path.name)
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API568 }),
569 })
570 })
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar571 .collect()
572 }
573
574 /// Push protection for a file committed through g1t (`commit_file`):
575 /// the refusal, naming each secret and where to bypass it, or `None`.
576 pub(crate) async fn protect_file(&self, repo: &Repo, actor: &User, path: &str, content: &[u8], commit: &str) -> Option<String> {
577 let patterns = compiled(&self.patterns_for(repo).await);
578 let found = scan_file(path, content, commit, &patterns);
579 if found.is_empty() {
580 return None;
581 }
582 let blocked = self.blocked(repo, Some(actor), found).await;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API583 if blocked.is_empty() {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar584 return None;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API585 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar586 Some(protection::explain(&blocked).join("\n"))
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API587 }
588
589 /// A page of the default branch's history, scanned for secrets.
590 pub(crate) async fn scan_history(&self, a: ScanHistoryArgs) -> Result<HistoryPage> {
591 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
592 return Ok(HistoryPage::default());
593 };
594 let git = self.store.open(&store_key(&repo)).await?;
595 let limit = a.limit.clamp(1, 100);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily596 // A page of a pushed range starts at its newest commit, and the
597 // history of the default branch at its head.
598 let start = a.after.or(a.from).unwrap_or_else(|| repo.default_branch.clone());
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API599 let mut commits = git.log(&start, limit + 1).await?;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily600 let mut next = (commits.len() > limit as usize).then(|| commits.pop().map(|commit| commit.hash)).flatten();
601 // A range ends where the branch was before the push.
602 if let Some(until) = a.until.as_deref()
603 && let Some(at) = commits.iter().position(|commit| commit.hash == until)
604 {
605 commits.truncate(at);
606 next = None;
607 }
608 if a.until.is_some() && next.as_deref() == a.until.as_deref() {
609 next = None;
610 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API611 let empty = Pack::default();
612 let objects = Objects { pack: &empty, repo: &git, reads: Cell::new(1) };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar613 let patterns = compiled(&a.patterns);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API614 let mut page = HistoryPage { next, ..HistoryPage::default() };
615 let mut seen = HashSet::new();
616 for (index, commit) in commits.iter().enumerate() {
617 let old_tree = match commit.parents.first() {
618 Some(parent) => match commits.get(index + 1).filter(|older| older.hash == *parent) {
619 Some(older) => Some(older.tree_hash.clone()),
620 None => objects.commit_tree(parent).await?,
621 },
622 None => None,
623 };
624 let changes = changed_files(&objects, old_tree, commit.tree_hash.clone()).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar625 for secret in scan_changes(&objects, &commit.hash, changes, &patterns).await? {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API626 if seen.insert(secret.fingerprint.clone()) {
627 page.secrets.push(secret);
628 }
629 }
630 page.commits += 1;
631 }
632 page.reads = objects.reads.get();
633 Ok(page)
634 }
635
636 /// The lockfiles on the default branch, outside vendored directories.
637 pub(crate) async fn find_lockfiles(&self, a: FindLockfilesArgs) -> Result<Lockfiles> {
638 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
639 return Ok(Lockfiles::default());
640 };
641 let git = self.store.open(&store_key(&repo)).await?;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar642 let at = a.git_ref.as_deref().unwrap_or(&repo.default_branch);
643 let Some(head) = git.log(at, 1).await?.into_iter().next() else {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API644 return Ok(Lockfiles::default());
645 };
646 let mut found = Vec::new();
647 let mut queue = VecDeque::from([(String::new(), head.tree_hash.clone(), 0usize)]);
648 while let Some((prefix, tree, depth)) = queue.pop_front() {
649 for entry in git.read_tree(&tree).await?.unwrap_or_default() {
650 match entry.kind {
651 EntryKind::Tree if depth < MAX_LOCKFILE_DEPTH && !SKIPPED_DIRECTORIES.contains(&entry.name.as_str()) => {
652 queue.push_back((format!("{prefix}{}/", entry.name), entry.hash, depth + 1));
653 }
654 EntryKind::Blob if Lockfile::for_path(&entry.name).is_some() && found.len() < MAX_LOCKFILES => {
655 found.push((format!("{prefix}{}", entry.name), entry.hash));
656 }
657 _ => {}
658 }
659 }
660 }
661 let texts = try_join_all(found.iter().map(|(_, hash)| git.read_blob(hash))).await?;
662 let files = found
663 .into_iter()
664 .zip(texts)
665 .filter_map(|((path, _), bytes)| {
666 let bytes = bytes.filter(|bytes| bytes.len() <= MAX_LOCKFILE_BYTES)?;
667 Some(LockfileText { path, text: String::from_utf8(bytes).ok()? })
668 })
669 .collect();
670 Ok(Lockfiles { commit: Some(head.hash), files })
671 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar672
673 /// A dry run of a custom pattern over the default branch's files, up to
674 /// [`MATCH_FILES`] files and [`MATCH_BYTES`] of text, skipping what
675 /// secret scanning skips. Nothing is recorded.
676 pub(crate) async fn match_pattern(&self, a: MatchPatternArgs) -> Result<PatternMatches> {
677 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
678 return Ok(PatternMatches::default());
679 };
680 let patterns = compiled(std::slice::from_ref(&a.pattern));
681 let Some(pattern) = patterns.first() else {
682 return Ok(PatternMatches::default());
683 };
684 let git = self.store.open(&store_key(&repo)).await?;
685 let Some(head) = git.log(&repo.default_branch, 1).await?.into_iter().next() else {
686 return Ok(PatternMatches::default());
687 };
688 let mut result = PatternMatches { commit: Some(head.hash.clone()), ..PatternMatches::default() };
689 let mut files = Vec::new();
690 let mut queue = VecDeque::from([(String::new(), head.tree_hash.clone())]);
691 while let Some((prefix, tree)) = queue.pop_front() {
692 for entry in git.read_tree(&tree).await?.unwrap_or_default() {
693 let path = format!("{prefix}{}", entry.name);
694 match entry.kind {
695 EntryKind::Tree if !SKIPPED_DIRECTORIES.contains(&entry.name.as_str()) => queue.push_back((format!("{path}/"), entry.hash)),
696 EntryKind::Blob | EntryKind::Exec if !g1t_scan::secrets::skipped_path(&path) => {
697 if files.len() == MATCH_FILES {
698 result.truncated = true;
699 } else {
700 files.push((path, entry.hash));
701 }
702 }
703 _ => {}
704 }
705 }
706 }
707 let mut bytes = 0usize;
708 let limit = a.limit.clamp(1, 200) as usize;
709 for batch in files.chunks(READS_AT_ONCE) {
710 if bytes > MATCH_BYTES || result.matches.len() >= limit {
711 result.truncated = true;
712 break;
713 }
714 let read = try_join_all(batch.iter().map(|(_, hash)| git.read_blob(hash))).await?;
715 for ((path, _), blob) in batch.iter().zip(read) {
716 let Some(blob) = blob else { continue };
717 bytes += blob.len();
718 result.files_scanned += 1;
719 let Some(text) = protection::text_of(path, &blob) else { continue };
720 let lines: Vec<&str> = text.lines().collect();
721 for hit in custom::scan_lines(text, std::slice::from_ref(pattern), |_| true) {
722 if result.matches.len() >= limit {
723 result.truncated = true;
724 break;
725 }
726 let line = lines.get(hit.line as usize - 1).copied().unwrap_or_default();
727 result.matches.push(PatternMatch { path: path.clone(), line: hit.line, preview: custom::masked_line(line, &hit.value) });
728 }
729 }
730 }
731 Ok(result)
732 }
733
734 /// Asks a landed secret's issuer whether it still works: finds it again
735 /// by its fingerprint at `commit`:`path` near `line`, and makes the
736 /// issuer's own read-only check over HTTPS. The value goes nowhere else.
737 pub(crate) async fn check_secret(&self, a: CheckSecretArgs) -> Result<SecretValidity> {
738 let unknown = |detail: &str| SecretValidity { validity: "unknown".to_owned(), detail: Some(detail.to_owned()) };
739 let Some(kind) = g1t_scan::secrets::SecretKind::parse(&a.kind) else {
740 return Ok(SecretValidity { validity: "unsupported".to_owned(), detail: None });
741 };
742 let Some(repo) = self.registry.by_id(&a.repo_id).await? else {
743 return Ok(unknown("no such repository"));
744 };
745 let git = self.store.open(&store_key(&repo)).await?;
746 let Some(bytes) = git.read_file(&a.commit, &a.path).await? else {
747 return Ok(unknown("the file is not at that commit"));
748 };
749 let Some(text) = protection::text_of(&a.path, &bytes) else {
750 return Ok(unknown("the file cannot be read as text"));
751 };
752 let near = |line: u32| line + 2 >= a.line && line <= a.line + 2;
753 let Some(hit) = g1t_scan::secrets::scan_lines(text, near).into_iter().find(|hit| hit.fingerprint() == a.fingerprint) else {
754 return Ok(unknown("the secret is no longer where it was found"));
755 };
756 let Some(probe) = g1t_scan::validity::check_for(kind, &hit.value) else {
757 return Ok(SecretValidity { validity: "unsupported".to_owned(), detail: None });
758 };
759 let headers = worker::Headers::new();
760 headers.set("user-agent", "g1t secret validity check (+https://docs.g1t.sh/guides/security/secret-protection/)")?;
761 for (name, value) in &probe.headers {
762 headers.set(name, value)?;
763 }
764 let mut init = worker::RequestInit::new();
765 init.with_method(if probe.method == "POST" { worker::Method::Post } else { worker::Method::Get }).with_headers(headers);
766 if let Some(body) = &probe.body {
767 init.with_body(Some(body.clone().into()));
768 }
769 let answer = async {
770 let mut response = worker::Fetch::Request(worker::Request::new_with_init(probe.url, &init)?).send().await?;
771 let status = response.status_code();
772 let body = if probe.reader == g1t_scan::validity::Reader::SlackOk { response.text().await.unwrap_or_default() } else { String::new() };
773 Ok::<_, worker::Error>((status, body))
774 }
775 .await;
776 Ok(match answer {
777 Ok((status, body)) => {
778 let validity = g1t_scan::validity::read(probe.reader, kind, status, &body);
779 SecretValidity {
780 validity: validity.as_str().to_owned(),
781 detail: (validity == g1t_scan::validity::Validity::Unknown).then(|| format!("the issuer answered {status}")),
782 }
783 }
784 Err(error) => unknown(&format!("the issuer could not be reached: {error}")),
785 })
786 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API787}
788
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar789/// Files a dry run reads, at most, and text in all.
790const MATCH_FILES: usize = 2_000;
791const MATCH_BYTES: usize = 20 * 1024 * 1024;
792
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API793#[cfg(test)]
794mod tests {
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer795 use std::cell::Cell;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API796 use std::collections::HashMap;
797 use std::future::Future;
798 use std::pin::pin;
799 use std::task::{Context, Poll, Waker};
800
801 use g1t_contracts::repos::{Branch, Commit, GitAccess, Signature, TreeEntry};
802 use g1t_scan::pack::{ObjectKind, TreeItem, encode_tree, object_id};
803
804 use super::*;
805 use crate::store::Scope;
806
807 /// Runs a future that never waits, as every call to the fake store is.
808 fn run<F: Future>(future: F) -> F::Output {
809 match pin!(future).as_mut().poll(&mut Context::from_waker(Waker::noop())) {
810 Poll::Ready(output) => output,
811 Poll::Pending => panic!("the fake store never waits"),
812 }
813 }
814
815 /// A repository held in memory.
816 #[derive(Default)]
817 struct FakeRepo {
818 blobs: HashMap<String, Vec<u8>>,
819 trees: HashMap<String, Vec<TreeEntry>>,
820 commits: HashMap<String, Commit>,
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer821 /// How often each kind of read was asked for.
822 blob_reads: Cell<u32>,
823 tree_reads: Cell<u32>,
824 log_reads: Cell<u32>,
Merge main into Artifacts Phase 2825 /// Each read waits once before it answers, as a store's does, so
826 /// that reads asked for together are in flight together.
827 waits: bool,
828 in_flight: Cell<u32>,
829 most_in_flight: Cell<u32>,
830 /// Every read fails as a busy store's does.
831 busy: bool,
832 }
833
834 impl FakeRepo {
835 async fn reading(&self) -> Result<()> {
836 if self.busy {
837 return Err(crate::resilience::Busy { rate_limited: true, retry_after: 5, read_only: false }.error("readBlob"));
838 }
839 if self.waits {
840 self.in_flight.set(self.in_flight.get() + 1);
841 self.most_in_flight.set(self.most_in_flight.get().max(self.in_flight.get()));
842 YieldOnce(false).await;
843 self.in_flight.set(self.in_flight.get() - 1);
844 }
845 Ok(())
846 }
847 }
848
849 /// Waits once, then is ready.
850 struct YieldOnce(bool);
851
852 impl Future for YieldOnce {
853 type Output = ();
854 fn poll(mut self: std::pin::Pin<&mut Self>, context: &mut Context<'_>) -> Poll<()> {
855 if self.0 {
856 return Poll::Ready(());
857 }
858 self.0 = true;
859 context.waker().wake_by_ref();
860 Poll::Pending
861 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API862 }
863
Merge main into Artifacts Phase 2864 /// Runs a future to its end, polling it until it is ready.
865 fn run_waiting<F: Future>(future: F) -> F::Output {
866 let mut future = pin!(future);
867 loop {
868 if let Poll::Ready(output) = future.as_mut().poll(&mut Context::from_waker(Waker::noop())) {
869 return output;
870 }
871 }
872 }
873
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API874 impl GitRepo for FakeRepo {
875 async fn access(&self, _scope: Scope) -> Result<GitAccess> {
876 unimplemented!()
877 }
878 async fn branches(&self) -> Result<Vec<Branch>> {
879 Ok(Vec::new())
880 }
881 async fn log(&self, git_ref: &str, _limit: u32) -> Result<Vec<Commit>> {
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer882 self.log_reads.set(self.log_reads.get() + 1);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API883 Ok(self.commits.get(git_ref).cloned().into_iter().collect())
884 }
885 async fn parents(&self, commit_hash: &str) -> Result<Option<Vec<String>>> {
886 Ok(self.commits.get(commit_hash).map(|commit| commit.parents.clone()))
887 }
888 async fn read_tree(&self, tree_hash: &str) -> Result<Option<Vec<TreeEntry>>> {
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer889 self.tree_reads.set(self.tree_reads.get() + 1);
Merge main into Artifacts Phase 2890 self.reading().await?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API891 Ok(self.trees.get(tree_hash).cloned())
892 }
893 async fn read_blob(&self, blob_hash: &str) -> Result<Option<Vec<u8>>> {
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer894 self.blob_reads.set(self.blob_reads.get() + 1);
Merge main into Artifacts Phase 2895 self.reading().await?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API896 Ok(self.blobs.get(blob_hash).cloned())
897 }
898 async fn read_file(&self, _git_ref: &str, _path: &str) -> Result<Option<Vec<u8>>> {
899 Ok(None)
900 }
901 async fn fork(&self, _target_key: &str) -> Result<()> {
902 Ok(())
903 }
904 }
905
906 /// Zlib with one stored (uncompressed) block, which is all a pack needs.
907 fn zlib(data: &[u8]) -> Vec<u8> {
908 let mut out = vec![0x78, 0x01, 0x01];
909 let length = data.len() as u16;
910 out.extend_from_slice(&length.to_le_bytes());
911 out.extend_from_slice(&(!length).to_le_bytes());
912 out.extend_from_slice(data);
913 let (mut a, mut b) = (1u32, 0u32);
914 for byte in data {
915 a = (a + u32::from(*byte)) % 65521;
916 b = (b + a) % 65521;
917 }
918 out.extend_from_slice(&((b << 16) | a).to_be_bytes());
919 out
920 }
921
922 fn header(code: u8, size: usize) -> Vec<u8> {
923 let mut out = Vec::new();
924 let mut byte = (code << 4) | (size & 15) as u8;
925 let mut rest = size >> 4;
926 while rest > 0 {
927 out.push(byte | 0x80);
928 byte = (rest & 0x7f) as u8;
929 rest >>= 7;
930 }
931 out.push(byte);
932 out
933 }
934
935 fn raw_id(id: &str) -> Vec<u8> {
936 id.as_bytes()
937 .chunks(2)
938 .map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap())
939 .collect()
940 }
941
942 enum Entry {
943 Whole(ObjectKind, Vec<u8>),
944 /// A ref-delta: base id and delta.
945 Delta(String, Vec<u8>),
946 }
947
948 /// A receive-pack request: one command, then the pack.
949 fn push(entries: &[Entry]) -> Vec<u8> {
950 let command = b"0000000000000000000000000000000000000000 4807077b296e6edbf410d55e72749d3e1170c291 refs/heads/main\0report-status side-band-64k\n";
951 let mut body = format!("{:04x}", command.len() + 4).into_bytes();
952 body.extend_from_slice(command);
953 body.extend_from_slice(b"0000PACK");
954 body.extend_from_slice(&2u32.to_be_bytes());
955 body.extend_from_slice(&(entries.len() as u32).to_be_bytes());
956 for entry in entries {
957 match entry {
958 Entry::Whole(kind, data) => {
959 let code = match kind {
960 ObjectKind::Commit => 1,
961 ObjectKind::Tree => 2,
962 ObjectKind::Blob => 3,
963 ObjectKind::Tag => 4,
964 };
965 body.extend(header(code, data.len()));
966 body.extend(zlib(data));
967 }
968 Entry::Delta(base, delta) => {
969 body.extend(header(7, delta.len()));
970 body.extend(raw_id(base));
971 body.extend(zlib(delta));
972 }
973 }
974 }
975 body.extend_from_slice(&[0u8; 20]);
976 body
977 }
978
979 fn key() -> String {
980 format!("AK{}", "IAZ7Q4N2XWLM3KDTRV")
981 }
982
983 fn commit(tree: &str, parent: Option<&str>) -> Vec<u8> {
984 let parent = parent.map(|parent| format!("parent {parent}\n")).unwrap_or_default();
985 format!("tree {tree}\n{parent}author A <a@example.com> 0 +0000\ncommitter A <a@example.com> 0 +0000\n\nchange\n").into_bytes()
986 }
987
988 #[test]
989 fn a_first_push_with_a_secret_is_found_by_file_and_line() {
990 let blob = format!("REGION=eu\nAWS_KEY={}\n", key()).into_bytes();
991 let blob_id = object_id(ObjectKind::Blob, &blob);
992 let tree = encode_tree(&[TreeItem { mode: "100644".into(), name: "config.env".into(), id: blob_id }]);
993 let tree_id = object_id(ObjectKind::Tree, &tree);
994 let body = push(&[
995 Entry::Whole(ObjectKind::Commit, commit(&tree_id, None)),
996 Entry::Whole(ObjectKind::Tree, tree),
997 Entry::Whole(ObjectKind::Blob, blob),
998 ]);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar999 let found = run(scan_push(&FakeRepo::default(), &body, &[])).unwrap();
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1000 assert_eq!(found.len(), 1);
1001 assert_eq!((found[0].path.as_str(), found[0].line, found[0].kind.as_str()), ("config.env", 2, "aws_access_key"));
1002 assert!(found[0].preview.starts_with("AKIA") && !found[0].preview.contains(&key()));
1003 }
1004
1005 #[test]
1006 fn a_thin_push_reports_only_the_lines_it_adds() {
1007 // The repository already has a file with a key in it (decided on
1008 // before); the push appends a line holding a second key.
1009 let old = format!("first={}\n", key()).into_bytes();
1010 let old_id = object_id(ObjectKind::Blob, &old);
1011 let second = format!("AK{}", "IAQ9W8E7R6T5Y4U3I2");
1012 let new = [old.clone(), format!("second={second}\n").into_bytes()].concat();
1013 let base_tree = vec![TreeEntry { name: "app.env".into(), hash: old_id.clone(), kind: EntryKind::Blob }];
1014 let base_tree_id = object_id(ObjectKind::Tree, &encode_tree(&[TreeItem { mode: "100644".into(), name: "app.env".into(), id: old_id.clone() }]));
1015 let parent_id = "c71546fcd893ef8b0f57388b65e620d759705dda".to_owned();
1016 let mut repo = FakeRepo::default();
1017 repo.blobs.insert(old_id.clone(), old.clone());
1018 repo.trees.insert(base_tree_id.clone(), base_tree);
1019 repo.commits.insert(
1020 parent_id.clone(),
1021 Commit {
1022 hash: parent_id.clone(),
1023 tree_hash: base_tree_id,
1024 message: String::new(),
1025 author: Signature { name: "A".into(), email: "a@example.com".into() },
1026 parents: Vec::new(),
1027 authored_at: String::new(),
1028 },
1029 );
1030 // A delta: copy the old file whole, then insert the new line.
1031 let added = format!("second={second}\n").into_bytes();
1032 let mut delta = vec![old.len() as u8, new.len() as u8, 0x80 | 0x10, old.len() as u8, added.len() as u8];
1033 delta.extend_from_slice(&added);
1034 let new_id = object_id(ObjectKind::Blob, &new);
1035 let tree = encode_tree(&[TreeItem { mode: "100644".into(), name: "app.env".into(), id: new_id }]);
1036 let tree_id = object_id(ObjectKind::Tree, &tree);
1037 let body = push(&[
1038 Entry::Whole(ObjectKind::Commit, commit(&tree_id, Some(&parent_id))),
1039 Entry::Whole(ObjectKind::Tree, tree),
1040 Entry::Delta(old_id, delta),
1041 ]);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1042 let found = run(scan_push(&repo, &body, &[])).unwrap();
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1043 assert_eq!(found.len(), 1, "{found:?}");
1044 assert_eq!((found[0].path.as_str(), found[0].line), ("app.env", 2));
1045 }
1046
1047 #[test]
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer1048 fn a_base_is_asked_for_as_what_the_pack_names_it_or_both_ways_at_once() {
1049 // A file's old version, which no tree in the pack names: asked for
1050 // as a blob and as a tree together, and found as a blob.
1051 let old = b"one
1052".to_vec();
1053 let old_id = object_id(ObjectKind::Blob, &old);
1054 let mut repo = FakeRepo::default();
1055 repo.blobs.insert(old_id.clone(), old.clone());
1056 let mut delta = vec![old.len() as u8, (old.len() + 4) as u8, 0x80 | 0x10, old.len() as u8, 4];
1057 delta.extend_from_slice(b"two
1058");
1059 let body = push(&[Entry::Delta(old_id.clone(), delta)]);
1060 let mut pack = crate::push_checks::read_pack(&body).unwrap();
1061 run(supply_bases(&mut pack, &repo)).unwrap();
1062 assert_eq!(pack.unresolved(), 0);
1063 assert_eq!((repo.blob_reads.get(), repo.tree_reads.get()), (1, 1));
1064
1065 // A directory the pack's root tree names as a tree: read as one.
1066 let file = object_id(ObjectKind::Blob, b"x");
1067 let listed = [TreeItem { mode: "100644".into(), name: "a".into(), id: file.clone() }];
1068 let sub = encode_tree(&listed);
1069 let sub_id = object_id(ObjectKind::Tree, &sub);
1070 let mut repo = FakeRepo::default();
1071 repo.trees.insert(sub_id.clone(), vec![TreeEntry { name: "a".into(), hash: file, kind: EntryKind::Blob }]);
1072 let root = encode_tree(&[TreeItem { mode: "40000".into(), name: "src".into(), id: sub_id.clone() }]);
1073 let delta = vec![sub.len() as u8, sub.len() as u8, 0x80 | 0x10, sub.len() as u8];
1074 let body = push(&[Entry::Whole(ObjectKind::Tree, root), Entry::Delta(sub_id, delta)]);
1075 let mut pack = crate::push_checks::read_pack(&body).unwrap();
1076 run(supply_bases(&mut pack, &repo)).unwrap();
1077 assert_eq!(pack.unresolved(), 0);
1078 assert_eq!((repo.blob_reads.get(), repo.tree_reads.get()), (0, 1));
1079 }
1080
1081 #[test]
Merge main into Artifacts Phase 21082 fn a_pack_sent_whole_is_checked_without_reading_a_base() {
1083 // What git sends when told `no-thin`: a delta's base is in the pack
1084 // with it. Here the second file is a delta on the first.
1085 let first = b"REGION=eu
1086".to_vec();
1087 let first_id = object_id(ObjectKind::Blob, &first);
1088 let added = format!("AWS_KEY={}
1089", key()).into_bytes();
1090 let second = [first.clone(), added.clone()].concat();
1091 let mut delta = vec![first.len() as u8, second.len() as u8, 0x80 | 0x10, first.len() as u8, added.len() as u8];
1092 delta.extend_from_slice(&added);
1093 let tree = encode_tree(&[
1094 TreeItem { mode: "100644".into(), name: "a.env".into(), id: first_id.clone() },
1095 TreeItem { mode: "100644".into(), name: "b.env".into(), id: object_id(ObjectKind::Blob, &second) },
1096 ]);
1097 let tree_id = object_id(ObjectKind::Tree, &tree);
1098 let body = push(&[
1099 Entry::Whole(ObjectKind::Commit, commit(&tree_id, None)),
1100 Entry::Whole(ObjectKind::Tree, tree),
1101 Entry::Whole(ObjectKind::Blob, first),
1102 Entry::Delta(first_id, delta),
1103 ]);
1104 let repo = FakeRepo::default();
1105 let mut pack = crate::push_checks::read_pack(&body).unwrap();
1106 let bases = run(supply_bases(&mut pack, &repo)).unwrap();
1107 assert_eq!(bases, Bases::default());
1108 assert!(!bases.thin());
1109 assert_eq!(pack.unresolved(), 0);
1110 let found = run(scan_pack(&repo, body.len(), &Ok(pack), &[])).unwrap();
1111 assert_eq!(found.len(), 1);
1112 assert_eq!((found[0].path.as_str(), found[0].line), ("b.env", 2));
1113 // Nothing was read from the store: not a base, not a file.
1114 assert_eq!((repo.blob_reads.get(), repo.tree_reads.get(), repo.log_reads.get()), (0, 0, 0));
1115 }
1116
1117 /// A pack of `count` deltas, each on a different base outside it.
1118 fn thin_pack(count: usize) -> Pack {
1119 let entries: Vec<Entry> = (1..=count)
1120 .map(|at| Entry::Delta(format!("{at:040x}"), vec![1, 2, 0x02, b'h', b'i']))
1121 .collect();
1122 crate::push_checks::read_pack(&push(&entries)).unwrap()
1123 }
1124
1125 #[test]
1126 fn a_large_thin_push_reads_a_bounded_number_of_bases_a_few_at_a_time() {
1127 // 300 bases the store does not have, none named by a tree: before,
1128 // each round asked for 500 at once, two reads each, three rounds.
1129 let mut pack = thin_pack(300);
1130 let repo = FakeRepo { waits: true, ..FakeRepo::default() };
1131 let bases = run_waiting(supply_bases(&mut pack, &repo)).unwrap();
1132 assert_eq!(bases, Bases { missing: 300, asked: MAX_BASES, left: 300 });
1133 assert!(bases.thin());
1134 // Asked once each, as a blob and as a tree, and never more at once
1135 // than a batch's.
1136 assert_eq!((repo.blob_reads.get(), repo.tree_reads.get()), (MAX_BASES as u32, MAX_BASES as u32));
1137 assert_eq!(repo.most_in_flight.get(), 2 * BASES_AT_ONCE as u32);
1138 }
1139
1140 #[test]
1141 fn a_busy_store_stops_the_reading_of_bases() {
1142 let mut pack = thin_pack(100);
1143 let repo = FakeRepo { busy: true, ..FakeRepo::default() };
1144 let bases = run(supply_bases(&mut pack, &repo)).unwrap();
1145 // One batch, then no more: the checks go on, and the store's own
1146 // answer to them says it is busy.
1147 assert_eq!(bases, Bases { missing: 100, asked: BASES_AT_ONCE, left: 100 });
1148 assert_eq!(repo.blob_reads.get(), BASES_AT_ONCE as u32);
1149 }
1150
1151 #[test]
A push is checked once and side by side: its pack is read and its bases fetched once for the rules, the workflow gate and the secret scan, which run together, other services are asked while the pack is read, and cache writes and rule records finish after git has its answer1152 fn the_commit_a_push_builds_on_is_read_once_however_many_commits_build_on_it() {
1153 // Two branches pushed at once, each one commit on the same parent.
1154 let parent_id = "c71546fcd893ef8b0f57388b65e620d759705dda".to_owned();
1155 let base_tree_id = object_id(ObjectKind::Tree, &[]);
1156 let mut repo = FakeRepo::default();
1157 repo.trees.insert(base_tree_id.clone(), Vec::new());
1158 repo.commits.insert(
1159 parent_id.clone(),
1160 Commit {
1161 hash: parent_id.clone(),
1162 tree_hash: base_tree_id,
1163 message: String::new(),
1164 author: Signature { name: "A".into(), email: "a@example.com".into() },
1165 parents: Vec::new(),
1166 authored_at: String::new(),
1167 },
1168 );
1169 let mut entries = Vec::new();
1170 for (name, line) in [("a.env", format!("KEY={}
1171", key())), ("b.txt", "nothing here
1172".to_owned())] {
1173 let blob = line.into_bytes();
1174 let tree = encode_tree(&[TreeItem { mode: "100644".into(), name: name.into(), id: object_id(ObjectKind::Blob, &blob) }]);
1175 entries.push(Entry::Whole(ObjectKind::Commit, commit(&object_id(ObjectKind::Tree, &tree), Some(&parent_id))));
1176 entries.push(Entry::Whole(ObjectKind::Tree, tree));
1177 entries.push(Entry::Whole(ObjectKind::Blob, blob));
1178 }
1179 let found = run(scan_push(&repo, &push(&entries), &[])).unwrap();
1180 assert_eq!(found.len(), 1);
1181 assert_eq!(found[0].path, "a.env");
1182 assert_eq!(repo.log_reads.get(), 1);
1183 }
1184
1185 #[test]
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1186 fn a_push_too_large_to_read_is_never_let_through_unread() {
1187 let body = vec![0u8; MAX_SCANNED_PUSH + 1];
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1188 let error = run(scan_push(&FakeRepo::default(), &body, &[])).unwrap_err();
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily1189 assert!(unscannable(&error));
1190 let (reason, messages) = crate::git_http::size_refusal(&crate::git_http::SizeViolation::Unscannable {
1191 size: body.len() as u64,
1192 cap: MAX_SCANNED_PUSH,
1193 });
1194 assert_eq!(reason, "the push is too large to check for secrets");
1195 assert!(messages.iter().any(|line| line.contains("100.0 MB")));
1196 assert!(messages.iter().any(|line| line.contains("Push in parts")));
1197 }
1198
1199 #[test]
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1200 fn a_push_without_secrets_or_a_pack_finds_nothing() {
1201 let blob = b"fn main() {}\n".to_vec();
1202 let blob_id = object_id(ObjectKind::Blob, &blob);
1203 let tree = encode_tree(&[TreeItem { mode: "100644".into(), name: "main.rs".into(), id: blob_id }]);
1204 let tree_id = object_id(ObjectKind::Tree, &tree);
1205 let body = push(&[
1206 Entry::Whole(ObjectKind::Commit, commit(&tree_id, None)),
1207 Entry::Whole(ObjectKind::Tree, tree),
1208 Entry::Whole(ObjectKind::Blob, blob),
1209 ]);
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1210 assert!(run(scan_push(&FakeRepo::default(), &body, &[])).unwrap().is_empty());
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1211 // A deletion sends commands and no pack.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar1212 assert!(run(scan_push(&FakeRepo::default(), b"0000", &[])).unwrap().is_empty());
1213 }
1214
1215 #[test]
1216 fn custom_patterns_are_found_in_a_push_and_a_committed_file() {
1217 let patterns = compiled(&[PatternSpec {
1218 id: "pat_1".into(),
1219 name: "Acme key".into(),
1220 pattern: "acme_[0-9a-f]{16}".into(),
1221 before: None,
1222 after: None,
1223 }]);
1224 let blob = b"token: acme_0123456789abcdef\n".to_vec();
1225 let blob_id = object_id(ObjectKind::Blob, &blob);
1226 let tree = encode_tree(&[TreeItem { mode: "100644".into(), name: "deploy.yml".into(), id: blob_id }]);
1227 let tree_id = object_id(ObjectKind::Tree, &tree);
1228 let body = push(&[
1229 Entry::Whole(ObjectKind::Commit, commit(&tree_id, None)),
1230 Entry::Whole(ObjectKind::Tree, tree),
1231 Entry::Whole(ObjectKind::Blob, blob.clone()),
1232 ]);
1233 let found = run(scan_push(&FakeRepo::default(), &body, &patterns)).unwrap();
1234 assert_eq!(found.len(), 1);
1235 assert_eq!((found[0].kind.as_str(), found[0].pattern_id.as_deref(), found[0].line), ("custom_pattern", Some("pat_1"), 1));
1236 assert_eq!(secret_label(&found[0]).unwrap(), "a match for the custom pattern \"Acme key\"");
1237 assert!(!found[0].preview.contains("0123456789abcdef"));
1238 // Without the pattern, nothing.
1239 assert!(run(scan_push(&FakeRepo::default(), &body, &[])).unwrap().is_empty());
1240 // A file committed through g1t is scanned for both.
1241 let file = format!("{blob}AWS={}\n", key(), blob = String::from_utf8(blob).unwrap());
1242 let found = scan_file(".g1t/workflows/deploy.yml", file.as_bytes(), "c0ffee", &patterns);
1243 let kinds: Vec<&str> = found.iter().map(|secret| secret.kind.as_str()).collect();
1244 assert_eq!(kinds, ["aws_access_key", "custom_pattern"]);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1245 }
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1246
1247 #[test]
1248 fn a_push_carrying_the_pushers_private_address_is_declined_with_a_masked_address() {
1249 let tree = encode_tree(&[]);
1250 let tree_id = object_id(ObjectKind::Tree, &tree);
1251 let mine = format!("tree {tree_id}
1252author S <Sam@Gmail.com> 0 +0000
1253committer S <sam@gmail.com> 0 +0000
1254
1255x
1256").into_bytes();
1257 let mine_id = object_id(ObjectKind::Commit, &mine);
1258 let guard = PushEmailGuard { emails: vec!["sam@gmail.com".into()], noreply: "1abc2def+sam@users.noreply.g1t.sh".into() };
1259 let body = push(&[Entry::Whole(ObjectKind::Commit, mine), Entry::Whole(ObjectKind::Tree, tree.clone())]);
1260 let (found, email) = exposed_address(&body, &guard).unwrap();
1261 assert_eq!(found, mine_id);
1262 let message = exposed_message(&found, &email, &guard.noreply);
1263 assert!(message[0].starts_with(&format!("push declined: commit {} would publish s***@gmail.com", &mine_id[..7])));
1264 assert!(message[1].contains("git config user.email 1abc2def+sam@users.noreply.g1t.sh"));
status.g1t.sh with incident management, invites that land you in the workspace, settings as pages, usage without quotas1265 assert!(message[2].contains("g1t.sh/settings/emails"));
Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look1266 // Someone else's commits, and no pack at all, go through.
1267 let theirs = push(&[Entry::Whole(ObjectKind::Commit, commit(&tree_id, None)), Entry::Whole(ObjectKind::Tree, tree)]);
1268 assert_eq!(exposed_address(&theirs, &guard), None);
1269 assert_eq!(exposed_address(b"0000", &guard), None);
1270 }
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API1271}

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