Skip to content
517 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//! Dependencies: reading a repository's lockfiles, asking OSV about every
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily2//! package in them, and recording what is vulnerable. The security updates
3//! that fix them are `security_updates`'s.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API4
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily5use std::collections::{BTreeSet, HashMap};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API6
7use g1t_contracts::repos::RepoPath;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily8use g1t_contracts::repos::{BlobArgs, BlobView};
9use g1t_contracts::security::{AlertState, FindLockfilesArgs, Lockfiles, VersionUpdatesState, VulnStatus, Vulnerability};
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar10use g1t_contracts::updates::DEPENDABOT_PATHS;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API11use g1t_contracts::time::rfc3339;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily12use g1t_contracts::work::{AddCommentArgs, Issue, IssueDetail, ViewArgs};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API13use g1t_contracts::{Outcome, User};
14use g1t_kit::now_ms;
15use g1t_scan::lockfiles::{Lockfile, Package, still_locked_check, test_command};
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily16use g1t_scan::osv::{self, Advisory};
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API17use serde_json::{Value, json};
18use worker::{Fetch, Headers, Method, Request, RequestInit, Result};
19
20use crate::Security;
21use crate::store::{RepoRow, VulnRow};
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily22use crate::updates;
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar23use crate::version_updates::Loaded;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API24
25/// OSV's records are fetched again after this long.
26const ADVISORY_MAX_AGE_MS: u64 = 7 * 24 * 60 * 60 * 1000;
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily27/// A record that names no fixed version is fetched again after a day, so
28/// a fix is noticed the day it is published.
29const UNFIXED_ADVISORY_MAX_AGE_MS: u64 = 24 * 60 * 60 * 1000;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API30/// Records fetched per scan, at most; the rest wait for the next one.
31const MAX_ADVISORY_FETCHES: usize = 150;
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put32/// CPU one call to OSV takes, sending it and reading its answer, in
33/// milliseconds (an estimate, rounded up). OSV itself is free, and a
34/// Worker's outgoing requests are not charged.
35const CPU_MS_PER_OSV_CALL: f64 = 2.0;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API36
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put37/// What a dependency check cost g1t, in millionths of a dollar, rounded
38/// up, at the prices in `history`: the CPU of its OSV calls, and the rows
39/// it writes (an advisory kept for each call at most, each vulnerability
40/// found, where the check stands and the month's usage).
41pub fn dependency_check_cost(calls: u32, found: usize) -> i64 {
42 use crate::history::{MICROS_PER_CPU_MS, MICROS_PER_ROW_WRITTEN};
43 let cpu = f64::from(calls) * CPU_MS_PER_OSV_CALL * MICROS_PER_CPU_MS;
44 let rows = (calls as usize + found + 2) as f64 * MICROS_PER_ROW_WRITTEN;
45 (cpu + rows).ceil() as i64
46}
47
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API48async fn osv_call(method: Method, url: &str, body: Option<&Value>) -> Result<Option<Value>> {
49 let headers = Headers::new();
50 headers.set("user-agent", "g1t (+https://g1t.sh)")?;
51 headers.set("accept", "application/json")?;
52 if body.is_some() {
53 headers.set("content-type", "application/json")?;
54 }
55 let mut init = RequestInit::new();
56 init.with_method(method).with_headers(headers);
57 if let Some(body) = body {
58 init.with_body(Some(body.to_string().into()));
59 }
60 let mut response = Fetch::Request(Request::new_with_init(url, &init)?).send().await?;
61 if response.status_code() == 404 {
62 return Ok(None);
63 }
64 if !(200..300).contains(&response.status_code()) {
65 return Err(worker::Error::RustError(format!("OSV answered {}", response.status_code())));
66 }
67 Ok(Some(response.json().await?))
68}
69
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily70/// Whether an OSV record names a version that fixes it, for any package.
71fn names_a_fix(record: &Value) -> bool {
72 record["affected"].as_array().into_iter().flatten().any(|affected| {
73 affected["ranges"]
74 .as_array()
75 .into_iter()
76 .flatten()
77 .any(|range| range["events"].as_array().into_iter().flatten().any(|event| event.get("fixed").is_some()))
78 })
79}
80
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API81/// One package in one lockfile.
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily82pub(crate) struct Located {
83 pub(crate) package: Package,
84 pub(crate) lockfile: Lockfile,
85 pub(crate) path: String,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API86}
87
88/// Every package the lockfiles resolve. A directory with a `go.mod` is read
89/// from it rather than from its `go.sum`, which lists versions not built.
90fn packages(files: &Lockfiles) -> Vec<Located> {
91 let go_mods: BTreeSet<&str> = files
92 .files
93 .iter()
94 .filter(|file| file.path.ends_with("go.mod"))
95 .map(|file| file.path.trim_end_matches("go.mod"))
96 .collect();
97 let mut located = Vec::new();
98 for file in &files.files {
99 let Some(lockfile) = Lockfile::for_path(&file.path) else { continue };
100 if lockfile == Lockfile::GoSum && go_mods.contains(file.path.trim_end_matches("go.sum")) {
101 continue;
102 }
103 for package in lockfile.parse(&file.text) {
104 located.push(Located { package, lockfile, path: file.path.clone() });
105 }
106 }
107 located
108}
109
110fn directory(path: &str) -> &str {
111 path.rsplit_once('/').map_or("", |(directory, _)| directory)
112}
113
114impl Security {
115 /// The ids of the vulnerabilities affecting each package, from OSV.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar116 pub(crate) async fn query_osv(&self, packages: &[Package]) -> Result<(Vec<Vec<String>>, u32)> {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API117 let mut ids = Vec::with_capacity(packages.len());
118 let mut calls = 0;
119 for (body, chunk) in osv::batch_bodies(packages).iter().zip(packages.chunks(osv::MAX_BATCH)) {
120 calls += 1;
121 let answer = osv_call(Method::Post, osv::QUERY_BATCH_URL, Some(body)).await?.unwrap_or(Value::Null);
122 let (mut found, more) = osv::read_batch(&answer, chunk.len());
123 // A package with many advisories is paged; fetch the rest.
124 for (index, token) in more.into_iter().take(20) {
125 let package = &chunk[index];
126 let query = json!({
127 "package": {"name": package.name, "ecosystem": package.ecosystem.osv()},
128 "version": package.version,
129 "page_token": token,
130 });
131 calls += 1;
132 if let Some(page) = osv_call(Method::Post, "https://api.osv.dev/v1/query", Some(&query)).await? {
133 found[index].extend(
134 page["vulns"].as_array().into_iter().flatten().filter_map(|v| v["id"].as_str().map(str::to_owned)),
135 );
136 }
137 }
138 ids.extend(found);
139 }
140 Ok((ids, calls))
141 }
142
143 /// OSV's record of each id, from the cache when it is fresh.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar144 pub(crate) async fn advisories(&self, ids: &BTreeSet<String>) -> Result<(HashMap<String, Value>, u32)> {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API145 let fresh_after = rfc3339(now_ms().saturating_sub(ADVISORY_MAX_AGE_MS));
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily146 let unfixed_fresh_after = rfc3339(now_ms().saturating_sub(UNFIXED_ADVISORY_MAX_AGE_MS));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API147 let mut records = HashMap::new();
148 let mut fetched = 0u32;
149 for id in ids {
150 if let Some(record) = self.store.advisory(id, &fresh_after).await? {
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily151 // One without a fix is asked about again daily; until then,
152 // or if asking fails below, the kept record stands.
153 let fixed = names_a_fix(&record);
154 let recent = !fixed && self.store.advisory(id, &unfixed_fresh_after).await?.is_some();
155 if fixed || recent || fetched as usize >= MAX_ADVISORY_FETCHES {
156 records.insert(id.clone(), record);
157 continue;
158 }
159 fetched += 1;
160 let refreshed = osv_call(Method::Get, &osv::vuln_url(id), None).await.ok().flatten();
161 if let Some(refreshed) = &refreshed {
162 self.store.keep_advisory(id, refreshed).await?;
163 }
164 records.insert(id.clone(), refreshed.unwrap_or(record));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API165 continue;
166 }
167 if fetched as usize >= MAX_ADVISORY_FETCHES {
168 continue;
169 }
170 fetched += 1;
171 if let Some(record) = osv_call(Method::Get, &osv::vuln_url(id), None).await? {
172 self.store.keep_advisory(id, &record).await?;
173 records.insert(id.clone(), record);
174 }
175 }
176 Ok((records, fetched))
177 }
178
179 /// Reads a repository's dependencies, records which are vulnerable,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily180 /// and starts security updates for those with a fix. Also reads
181 /// `.g1t/dependencies.yml`. Returns what went wrong, for the Security
182 /// page, if anything did.
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API183 pub async fn scan_dependencies(&self, repo: &RepoRow) -> Result<Option<String>> {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar184 let files: Lockfiles = g1t_kit::call(&self.repos, "find_lockfiles", &FindLockfilesArgs { repo_id: repo.repo_id.clone(), git_ref: None }).await?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API185 let paths: Vec<String> = files.files.iter().map(|file| file.path.clone()).collect();
186 let located = packages(&files);
187 let unique: Vec<Package> = located.iter().map(|l| l.package.clone()).collect::<BTreeSet<_>>().into_iter().collect();
188 let outcome = async {
189 let (ids, calls) = self.query_osv(&unique).await?;
190 let by_package: HashMap<&Package, &Vec<String>> = unique.iter().zip(ids.iter()).collect();
191 let wanted: BTreeSet<String> = ids.iter().flatten().cloned().collect();
192 let (records, fetched) = self.advisories(&wanted).await?;
193 let mut found = Vec::new();
194 for item in &located {
195 for id in by_package.get(&item.package).into_iter().flat_map(|ids| ids.iter()) {
196 let Some(advisory) = records.get(id).and_then(|record| osv::read_vuln(record, &item.package)) else {
197 continue;
198 };
199 found.push(vulnerability(&repo.repo_id, item, &advisory));
200 }
201 }
202 Ok::<_, worker::Error>((found, calls + fetched))
203 }
204 .await;
205 let (found, calls) = match outcome {
206 Ok(result) => result,
207 Err(error) => {
208 let problem = format!("The dependencies could not be checked: {error}");
209 self.store
210 .set_dependencies_scanned(&repo.repo_id, files.commit.as_deref(), &paths, Some(&problem))
211 .await?;
212 return Ok(Some(problem));
213 }
214 };
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar215 let before: BTreeSet<String> = self.store.open_vulnerabilities(&repo.repo_id).await?.into_iter().map(|row| row.id).collect();
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API216 self.store.replace_vulnerabilities(&repo.repo_id, &found).await?;
217 self.store.set_dependencies_scanned(&repo.repo_id, files.commit.as_deref(), &paths, None).await?;
Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches218 // g1t's update pull requests whose alerts are now fixed or dismissed,
219 // or whose versions the lockfiles already have, close first, so that
220 // nothing later in the scan can keep them open.
221 if let Err(error) = self.resolve_updates(repo, Some((&located, &paths))).await {
222 worker::console_error!("security: updates of {} not resolved: {error}", repo.repo_id);
223 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar224 if let Err(error) = self.record_graph(repo, &files).await {
225 worker::console_error!("security: dependency graph of {} not kept: {error}", repo.repo_id);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API226 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar227 self.vulnerabilities_changed(repo, &before).await?;
228 self.meter(&repo.namespace, 0, 0, calls, dependency_check_cost(calls, found.len())).await?;
229 // The dependency update file says how security updates are made too.
230 let rules = match files.commit.as_deref() {
231 Some(commit) => self.read_version_updates(repo, commit).await?,
232 None => None,
233 };
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily234 // No security updates on an archived (read-only) or deleted
A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so.235 // repository, nor on a mirror until it is taken over or moved to
236 // g1t (`g1t_contracts::mirrors`: a standby mirror is an exact copy
237 // that opens nothing of its own); ones in flight for packages no
238 // longer vulnerable are closed either way.
239 let takes_updates = self.takes_updates(repo).await?.is_ok();
240 self.security_updates(repo, repo.upkeep != 0 && takes_updates, rules.as_ref()).await?;
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API241 Ok(None)
242 }
243
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar244 /// Reads the dependency update file at `commit` and keeps what it
245 /// says; see `updates`. Its entries' schedules start or move. Returns
246 /// it, when it has no problems.
247 pub(crate) async fn read_version_updates(&self, repo: &RepoRow, commit: &str) -> Result<Option<Loaded>> {
248 let path = RepoPath { namespace: repo.namespace.clone(), name: repo.name.clone() };
249 let asks: Vec<BlobArgs> = DEPENDABOT_PATHS
250 .iter()
251 .map(|file| BlobArgs {
252 path: path.clone(),
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily253 viewer: Some(User::system(&repo.namespace)),
254 git_ref: commit.to_owned(),
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar255 file_path: (*file).to_owned(),
256 })
257 .collect();
258 let reads = asks.iter().map(|ask| g1t_kit::call::<_, Outcome<BlobView>>(&self.repos, "blob", ask));
259 let mut found = Vec::new();
260 let mut default_branch = None;
261 for (file, read) in DEPENDABOT_PATHS.iter().zip(futures_util::future::join_all(reads).await) {
262 if let Outcome::Ok(blob) = read? {
263 default_branch = Some(blob.repo.default_branch.clone());
264 found.push(((*file).to_owned(), blob.text));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API265 }
266 }
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar267 let (mut state, config) = match updates::choose(found) {
268 Some(((file, text), ignored)) => {
269 let (state, config) = updates::state(&file, text.as_deref(), ignored, &repo.repo_id, default_branch.as_deref());
270 (state, config.map(|config| (config, file)))
271 }
272 None => (VersionUpdatesState::default(), None),
273 };
274 state.read_at = Some(crate::store::now());
275 state.commit = Some(commit.to_owned());
276 self.store.set_version_updates(&repo.repo_id, &state).await?;
277 self.schedule_entries(repo, config.as_ref().map(|(config, _)| config), default_branch.as_deref()).await?;
278 Ok(config.map(|(config, file)| Loaded { config, file, default_branch: default_branch.unwrap_or_default() }))
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API279 }
280
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily281 pub(crate) async fn issue(&self, actor: &User, repo: &RepoPath, number: u32) -> Result<Option<Issue>> {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API282 let found: Outcome<IssueDetail> = g1t_kit::call(
283 &self.work,
284 "get_issue",
285 &ViewArgs { repo: repo.clone(), number, viewer: Some(actor.clone()), after_seq: 0 },
286 )
287 .await?;
288 Ok(found.into_result().ok().map(|detail| detail.issue))
289 }
290
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily291 pub(crate) async fn comment(&self, actor: &User, repo: &RepoPath, number: u32, body: String) -> Result<()> {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API292 let _: Outcome<Value> = g1t_kit::call(
293 &self.work,
294 "add_comment",
295 &AddCommentArgs {
296 actor: actor.clone(),
297 repo: repo.clone(),
298 number,
299 body,
300 path: None,
301 line: None,
302 verdict: None,
303 },
304 )
305 .await?;
306 Ok(())
307 }
308}
309
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar310/// The event that tells of a vulnerability alert.
311pub(crate) fn vulnerability_event(repo: &RepoRow, vuln: &Vulnerability) -> g1t_contracts::security_suite::SecurityEvent {
312 g1t_contracts::security_suite::SecurityEvent {
313 repo_id: repo.repo_id.clone(),
314 alert_id: vuln.id.clone(),
315 alert_type: g1t_contracts::security_suite::AlertType::Vulnerability.as_str().to_owned(),
316 severity: vuln.severity.clone(),
317 title: format!("{} {} in {}: {}", vuln.package, vuln.version, vuln.manifest, vuln.advisory),
318 link: crate::suite::link(repo, &format!("vulnerabilities?finding={}", vuln.id)),
319 path: Some(vuln.manifest.clone()),
320 state: vuln.state.as_str().to_owned(),
321 ..Default::default()
322 }
323}
324
325impl Security {
326 /// Tells of vulnerability alerts a dependency read opened or fixed,
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.327 /// given the ids that were open before it: an event each for webhooks
328 /// (the first `MAX_SUMMARY_IDS` of each), and one summary of the read
329 /// for the inbox, which carries the counts whatever the number.
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar330 async fn vulnerabilities_changed(&self, repo: &RepoRow, before: &BTreeSet<String>) -> Result<()> {
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.331 use g1t_contracts::security_suite::{AlertType, MAX_SUMMARY_IDS};
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar332 let after: Vec<VulnRow> = self.store.open_vulnerabilities(&repo.repo_id).await?;
333 let now: BTreeSet<&str> = after.iter().map(|row| row.id.as_str()).collect();
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.334 // Worst first, as the rows come: the summary's headline is the worst new one.
335 let opened: Vec<String> = after.iter().map(|row| row.id.clone()).filter(|id| !before.contains(id)).collect();
336 let mut fixed: Vec<String> = Vec::new();
337 for id in before.iter().filter(|id| !now.contains(id.as_str())) {
338 if let Some(vuln) = self.store.vulnerability(&repo.repo_id, id).await?
339 && vuln.status == VulnStatus::Fixed
340 {
341 fixed.push(id.clone());
342 }
343 }
344 for id in opened.iter().take(MAX_SUMMARY_IDS) {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar345 if let Some(vuln) = self.store.vulnerability(&repo.repo_id, id).await? {
346 self.alert_event(AlertType::Vulnerability, "created", repo, vulnerability_event(repo, &vuln), None).await;
347 }
348 }
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.349 for id in fixed.iter().take(MAX_SUMMARY_IDS) {
350 if let Some(vuln) = self.store.vulnerability(&repo.repo_id, id).await? {
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar351 self.alert_event(AlertType::Vulnerability, "fixed", repo, vulnerability_event(repo, &vuln), None).await;
352 }
353 }
Security tells people the way they can live with. Each scan publishes one summary event per alert type (secret_scanning_alert.summary, code_scanning_alert.summary, vulnerability_alert.summary: what it opened, by severity, what it fixed, what is open now, the worst new alert and the newest one's title, with the owners and security managers to tell) beside the per-alert events webhooks keep, so the inbox holds one thread per repository and alert type that the next scan brings up to date instead of one item per finding, and the ids are capped at 200 while the counts are not. Critical and high alerts and a refused push reach the inbox at once (and toast like a mention, as the new security notification kind); medium and low wait for each person's security digest, one inbox item and email per workspace, daily or weekly as they choose in their notification settings, at once, or never; a scan that opened nothing refreshes the thread quietly ("2 of 3 fixed") and marks it done when nothing is left open; and security alerts are emailed by default. A repository can turn alerts for secrets found in its history off while push protection stays on. Reporting arrives: security advisories (G1TSA ids, drafts published and withdrawn by Admins and security managers, visible to anyone who can see the repository once published) and private vulnerability reporting (a repository setting; anyone who can see it may report, signed out with an email on a public one, five a day each, and the owners and security managers are told at once), each with its RPC methods, REST routes under /repos/{owner}/{name}/security/advisories and /security/reports, MCP actions, scopes and reference examples.354 let changed = crate::summaries::Changed { scan: "dependencies", opened, fixed, ..crate::summaries::Changed::default() };
355 self.summarize(repo, AlertType::Vulnerability, changed, None).await
Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar356 }
357}
358
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API359fn vulnerability(repo_id: &str, item: &Located, advisory: &Advisory) -> Vulnerability {
360 Vulnerability {
361 id: String::new(),
362 repo_id: repo_id.to_owned(),
363 ecosystem: item.package.ecosystem.osv().to_owned(),
364 package: item.package.name.clone(),
365 version: item.package.version.clone(),
366 manifest: item.path.clone(),
367 advisory: advisory.display_id.clone(),
368 osv_id: advisory.id.clone(),
369 summary: advisory.summary.clone(),
370 severity: advisory.severity.as_str().to_owned(),
371 fixed_version: advisory.fixed.clone(),
372 status: VulnStatus::Open,
373 issue: None,
374 found_at: String::new(),
375 fixed_at: None,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily376 state: AlertState::Open,
377 dismissed_by: None,
378 dismissed_reason: None,
379 dismissed_comment: None,
380 dismissed_at: None,
381 update: None,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API382 }
383}
384
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily385/// The issue's body, for when raising the version is not enough, written
386/// for the agent that takes it as much as for a person, ending with what
387/// done means: commands that show no lockfile still resolves a vulnerable
388/// version, and the project's tests. The pull request merges on the
389/// repository's required checks, like any other.
390pub(crate) fn issue_text(ecosystem: &str, package: &str, target: &str, vulns: &[&VulnRow], located: &[Located]) -> String {
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API391 let mut body = format!(
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily392 "`{package}` ({ecosystem}) has known vulnerabilities with a fix in **{target}**, and raising its version alone \
393 does not pass this project's checks. Upgrade it to {target} or later everywhere it is locked, and change the \
394 code that depends on it, keeping other changes to what the upgrade needs.\n\n"
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API395 );
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily396 body.push_str(&advisory_table(vulns));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API397 let mut checks = Vec::new();
398 let mut manifests = BTreeSet::new();
399 let mut tests = BTreeSet::new();
400 for vuln in vulns {
401 let Some(item) = located
402 .iter()
403 .find(|item| item.path == vuln.manifest && item.package.name == vuln.package && item.package.version == vuln.version)
404 else {
405 continue;
406 };
407 if manifests.insert((item.path.clone(), item.package.version.clone())) {
408 checks.push(still_locked_check(item.lockfile, &item.path, &item.package.name, &item.package.version));
409 }
410 if let Some(test) = test_command(item.lockfile, directory(&item.path)) {
411 tests.insert(test);
412 }
413 }
414 let locked: Vec<String> = manifests.iter().map(|(path, version)| format!("`{path}` ({version})")).collect();
415 body.push_str(&format!("\nLocked in: {}.\n", locked.join(", ")));
416 body.push_str(
Fast pages, required checks on the branch, self-hosted runners, honest incidents417 "\nIf the fix needs a major upgrade that breaks the build, change the code that depends on it in the same pull request.",
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API418 );
419 checks.extend(tests);
Fast pages, required checks on the branch, self-hosted runners, honest incidents420 let mut done = vec!["No lockfile resolves a vulnerable version, and the tests still pass.".to_owned()];
421 done.extend(g1t_contracts::work::commands_pass(&checks));
422 let mut body = g1t_contracts::work::with_definition_of_done(&body, &done);
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily423 body.push_str("\n\n---\n_Opened by g1t's security updates. Turn them off for this project on its Security page._");
Fast pages, required checks on the branch, self-hosted runners, honest incidents424 body
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API425}
426
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily427/// The advisories a package's vulnerabilities name, as a table.
428pub(crate) fn advisory_table(vulns: &[&VulnRow]) -> String {
429 let mut table = "| Advisory | Severity | Affected | Fixed in | Summary |\n| --- | --- | --- | --- | --- |\n".to_owned();
430 let mut seen = BTreeSet::new();
431 for vuln in vulns {
432 if !seen.insert((vuln.advisory.clone(), vuln.version.clone())) {
433 continue;
434 }
435 table.push_str(&format!(
436 "| [{}]({}) | {} | {} | {} | {} |\n",
437 vuln.advisory,
438 osv::page_url(&vuln.osv_id),
439 vuln.severity,
440 vuln.version,
441 vuln.fixed_version.as_deref().unwrap_or("none yet"),
442 vuln.summary.replace('|', "\\|").replace('\n', " "),
443 ));
444 }
445 table
446}
447
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API448#[cfg(test)]
449mod tests {
450 use super::*;
451 use g1t_contracts::security::LockfileText;
452
453 #[test]
Team plan, an open-source pool, monthly trials and honest metering; the sidebar for everyone; a workspace that stays put454 fn scans_cost_their_cpu_and_the_rows_they_write() {
455 // 10 OSV calls and 3 vulnerabilities: 0.4 of CPU, 15 rows.
456 assert_eq!(dependency_check_cost(10, 3), 16);
457 assert_eq!(dependency_check_cost(0, 0), 2);
458 // A page of 25 commits that read 100 objects and found nothing:
459 // 10 of CPU and 2 rows. The old placeholder charged 100.
460 assert_eq!(crate::history::history_page_cost(100, 0), 12);
461 assert_eq!(crate::history::history_page_cost(0, 1), 3);
462 }
463
464 #[test]
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API465 fn go_sum_is_skipped_beside_go_mod() {
466 let files = Lockfiles {
467 commit: None,
468 files: vec![
469 LockfileText { path: "go.mod".into(), text: "require golang.org/x/net v0.7.0\n".into() },
470 LockfileText { path: "go.sum".into(), text: "golang.org/x/net v0.1.0 h1:x=\n".into() },
471 LockfileText { path: "tools/go.sum".into(), text: "golang.org/x/text v0.3.0 h1:x=\n".into() },
472 ],
473 };
474 let found: Vec<String> = packages(&files).iter().map(|l| format!("{}:{}", l.path, l.package.version)).collect();
475 assert_eq!(found, ["go.mod:v0.7.0", "tools/go.sum:v0.3.0"]);
476 }
477
478 #[test]
479 fn the_issue_names_the_advisories_and_checks_the_lockfile() {
480 let row = VulnRow {
481 id: "vul_1".into(),
482 repo_id: "rep_1".into(),
483 ecosystem: "npm".into(),
484 package: "lodash".into(),
485 version: "4.17.20".into(),
486 manifest: "web/package-lock.json".into(),
487 osv_id: "GHSA-35jh-r3h4-6jhm".into(),
488 advisory: "GHSA-35jh-r3h4-6jhm".into(),
489 summary: "Command Injection in lodash".into(),
490 severity: "high".into(),
491 fixed_version: Some("4.17.21".into()),
492 status: "open".into(),
493 found_at: "2026-10-04T00:00:00Z".into(),
494 fixed_at: None,
495 number: None,
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily496 dismiss_reason: None,
497 dismiss_comment: None,
498 dismissed_by: None,
499 dismissed_at: None,
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API500 };
501 let located = vec![Located {
502 package: Package { ecosystem: g1t_scan::lockfiles::Ecosystem::Npm, name: "lodash".into(), version: "4.17.20".into() },
503 lockfile: Lockfile::PackageLock,
504 path: "web/package-lock.json".into(),
505 }];
Fast pages, required checks on the branch, self-hosted runners, honest incidents506 let body = issue_text("npm", "lodash", "4.17.21", &[&row], &located);
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API507 assert!(body.contains("[GHSA-35jh-r3h4-6jhm](https://osv.dev/vulnerability/GHSA-35jh-r3h4-6jhm) | high | 4.17.20 | 4.17.21"));
508 assert!(body.contains("`web/package-lock.json` (4.17.20)"));
Fast pages, required checks on the branch, self-hosted runners, honest incidents509 let (_, done) = body.split_once("## Definition of done\n\n").unwrap();
510 let items: Vec<&str> = done.lines().take_while(|line| line.starts_with("- ")).collect();
511 assert_eq!(items.len(), 3);
512 assert!(items[1].contains("node_modules/lodash") && items[1].contains("'web/package-lock.json'"));
513 assert_eq!(items[2], "- `cd 'web' && npm ci && npm test --if-present` passes.");
514 assert!(body.ends_with("on its Security page._"));
Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily515 assert!(advisory_table(&[&row]).contains("| [GHSA-35jh-r3h4-6jhm](https://osv.dev/vulnerability/GHSA-35jh-r3h4-6jhm) | high |"));
Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API516 }
517}

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