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 API | 1 | //! The security service: what g1t finds wrong in a repository, and the |
| 2 | //! upkeep that fixes it without a person. | |
| 3 | //! | |
| 4 | //! - Secrets. The repos service refuses pushes that add one | |
| 5 | //! (`push_blocked` says which were allowed and records the rest), and | |
| 6 | //! each repository's history is scanned once, in the background. | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 7 | //! A push too large to scan first is let through, and its new commits |
| 8 | //! are scanned after they land (`history::advance_push_scan`). | |
| 9 | //! - Alerts are open, dismissed (with a reason, a comment and who) or | |
| 10 | //! fixed, and each keeps an activity log. Likely test values are listed | |
| 11 | //! apart and never block a push or count as critical. | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 12 | //! - Dependencies. On every push to a default branch, and daily, the |
| 13 | //! lockfiles are read and every package checked against OSV. Each | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 14 | //! vulnerable package with a fix gets a security update: g1t itself |
| 15 | //! (`User::system`) opens a pull request raising its version, made in a | |
| 16 | //! sandbox (the runner's `bump`), which lands through the branch's | |
| g1t is one name: its agent's work, commits and comments show as @g1t, and nobody can claim g1t or g1t-agent | 17 | //! required checks. Only when code has to change is g1t put on an |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 18 | //! issue for it. |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 19 | //! |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 20 | //! - The security suite (`g1t_contracts::security_suite`): custom secret |
| 21 | //! patterns (`patterns`), push protection bypasses, their review and | |
| 22 | //! validity checks (`secret_alerts`), code scanning from SARIF uploads | |
| 23 | //! and its pull request check (`code_scanning`), the dependency graph, | |
| 24 | //! its SBOM and dependency review (`supply_chain`), "Fix with g1t" | |
| 25 | //! (`fixes`), and settings with the workspace's overview (`overview`). | |
| 26 | //! On private repositories its paid parts need the workspace's Security | |
| 27 | //! and quality activation (`suite`); it tells people through events. | |
| 28 | //! | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 29 | //! Other services reach it over `POST /rpc/<method>`; see |
| 30 | //! `g1t_contracts::security`. | |
| 31 | ||
| 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. | 32 | mod advisories; |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 33 | mod code_scanning; |
| 34 | mod config; | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 35 | mod deps; |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 36 | mod fixes; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 37 | mod history; |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 38 | mod manifests; |
| 39 | mod overview; | |
| 40 | mod patterns; | |
| 41 | mod planning; | |
| 42 | mod pull_text; | |
| Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches | 43 | mod resolved; |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 44 | mod ranges; |
| 45 | mod registries; | |
| 46 | mod schedule; | |
| 47 | mod secret_alerts; | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 48 | mod security_updates; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 49 | mod store; |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 50 | mod suite; |
| 51 | mod suite_store; | |
| 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. | 52 | mod summaries; |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 53 | mod supply_chain; |
| 54 | mod timezones; | |
| 55 | mod update_store; | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 56 | mod updates; |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 57 | mod version_updates; |
| 58 | mod yaml; | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 59 | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 60 | use g1t_contracts::access::{self, Capability}; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 61 | use g1t_contracts::events::{Event, WorkspaceRenamed}; |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 62 | use g1t_contracts::security::UPDATE_BRANCH_PREFIX; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 63 | use g1t_contracts::repos::{GetArgs, PathByIdArgs, Repo, RepoPath, RepoStatus, StatusByIdArgs}; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 64 | use g1t_contracts::security::*; |
| 65 | use g1t_contracts::time::rfc3339; | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 66 | use g1t_contracts::{FailureCode, Outcome, User}; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 67 | use g1t_kit::{args, now_ms, reply, rpc_method}; |
| 68 | use serde::Deserialize; | |
| 69 | use worker::{Context, Env, Fetcher, MessageBatch, Request, Response, Result, ScheduleContext, ScheduledEvent, event}; | |
| 70 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 71 | use store::{Activity, RepoRow, Store}; |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 72 | |
| 73 | /// Repositories whose history is continued per sweep, and pages each. | |
| 74 | const HISTORIES_PER_SWEEP: u32 = 5; | |
| 75 | const PAGES_PER_SWEEP: u32 = 4; | |
| 76 | /// Repositories whose dependencies are read again per sweep. | |
| 77 | const DEPENDENCIES_PER_SWEEP: u32 = 10; | |
| 78 | const DAY_MS: u64 = 24 * 60 * 60 * 1000; | |
| 79 | const MAX_REASON_CHARS: usize = 500; | |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 80 | /// What seeing and dismissing a repository's findings takes: the Write |
| 81 | /// role, as on GitHub, or a security manager of its workspace. Changing its | |
| 82 | /// security settings takes Admin (`ManageSecurity`). | |
| 83 | const SEE_FINDINGS: Capability = Capability::SecurityAlerts; | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 84 | |
| 85 | pub struct Security { | |
| 86 | store: Store, | |
| 87 | identity: Fetcher, | |
| 88 | repos: Fetcher, | |
| 89 | work: Fetcher, | |
| 90 | runner: Fetcher, | |
| 91 | billing: Fetcher, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 92 | actions: Fetcher, |
| 93 | /// The events service: security events for webhooks and the inbox, | |
| 94 | /// and audit entries. Optional so a deployment without the binding | |
| 95 | /// still scans. | |
| 96 | events: Option<Fetcher>, | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 97 | } |
| 98 | ||
| 99 | fn fail<T>(code: FailureCode, message: impl Into<String>) -> Outcome<T> { | |
| 100 | Outcome::fail(code, message) | |
| 101 | } | |
| 102 | ||
| 103 | /// `git.push`, as far as this service reads it. | |
| 104 | #[derive(Deserialize)] | |
| 105 | #[serde(rename_all = "camelCase")] | |
| 106 | struct Pushed { | |
| 107 | repo_id: String, | |
| 108 | #[serde(default)] | |
| 109 | default_branch: bool, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 110 | #[serde(default, rename = "ref")] |
| 111 | git_ref: String, | |
| 112 | #[serde(default)] | |
| 113 | before: Option<String>, | |
| 114 | #[serde(default)] | |
| 115 | after: String, | |
| 116 | /// Too large to scan before it was stored. | |
| 117 | #[serde(default)] | |
| 118 | unscanned: bool, | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 119 | } |
| 120 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 121 | /// `pull.merged`, `pull.closed` and `checks.completed`, as far as this |
| 122 | /// service reads them. | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 123 | #[derive(Deserialize)] |
| 124 | #[serde(rename_all = "camelCase")] | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 125 | struct PullHappened { |
| 126 | repo_id: String, | |
| 127 | number: u32, | |
| 128 | #[serde(default)] | |
| 129 | status: Option<String>, | |
| 130 | } | |
| 131 | ||
| 132 | /// The longest comment a dismissal keeps. | |
| 133 | const MAX_COMMENT_CHARS: usize = MAX_REASON_CHARS; | |
| 134 | /// Activity rows the Security page reads, newest first. | |
| 135 | const ACTIVITY_SHOWN: u32 = 500; | |
| 136 | ||
| 137 | #[derive(Deserialize)] | |
| 138 | #[serde(rename_all = "camelCase")] | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 139 | struct Created { |
| 140 | repo_id: String, | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 141 | /// Absent from events older than the field. |
| 142 | #[serde(default)] | |
| 143 | is_private: Option<bool>, | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 144 | } |
| 145 | ||
| 146 | impl Security { | |
| 147 | fn new(env: &Env) -> Result<Self> { | |
| 148 | Ok(Security { | |
| 149 | store: Store { db: env.d1("DB")? }, | |
| 150 | identity: env.service("IDENTITY")?, | |
| 151 | repos: env.service("REPOS")?, | |
| 152 | work: env.service("WORK")?, | |
| 153 | runner: env.service("RUNNER")?, | |
| 154 | billing: env.service("BILLING")?, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 155 | actions: env.service("ACTIONS")?, |
| 156 | events: env.service("EVENTS").ok(), | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 157 | }) |
| 158 | } | |
| 159 | ||
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 160 | /// The repository at `path`, recorded here, if `viewer` may see its |
| 161 | /// findings (the Write role) and do `capability`. Findings are for those | |
| 162 | /// who can change the code: to anyone else the page does not exist, | |
| 163 | /// public repository or not. | |
| 164 | async fn member_repo( | |
| 165 | &self, | |
| 166 | path: &RepoPath, | |
| 167 | viewer: &Option<User>, | |
| 168 | capability: Capability, | |
| 169 | ) -> Result<Outcome<RepoRow>> { | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 170 | let hidden = || fail(FailureCode::NotFound, "Repository not found."); |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 171 | if viewer.is_none() { |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 172 | return Ok(hidden()); |
| 173 | } | |
| 174 | let repo: Outcome<Repo> = | |
| 175 | g1t_kit::call(&self.repos, "get", &GetArgs { path: path.clone(), viewer: viewer.clone() }).await?; | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 176 | let repo = match repo { |
| 177 | Outcome::Ok(repo) if repo.fork_of.is_none() => repo, | |
| 178 | _ => return Ok(hidden()), | |
| 179 | }; | |
| 180 | if !access::can(viewer.as_ref(), &repo, SEE_FINDINGS) { | |
| 181 | return Ok(hidden()); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 182 | } |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 183 | if !access::can(viewer.as_ref(), &repo, capability) { |
| 184 | return Ok(fail( | |
| 185 | FailureCode::Forbidden, | |
| 186 | access::needs(capability, &format!("{}/{}", repo.namespace, repo.name)), | |
| 187 | )); | |
| 188 | } | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 189 | let row = self.store.register(&repo.id, &repo.namespace, &repo.name).await?; |
| 190 | self.store.set_private(&repo.id, repo.is_private).await?; | |
| 191 | Ok(Outcome::Ok(row)) | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 192 | } |
| 193 | ||
| 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. | 194 | /// Where the repository stands: archived, deleted, a mirror. When repos |
| 195 | /// cannot say, it is taken as an ordinary active repository. | |
| 196 | async fn status(&self, repo_id: &str) -> Result<RepoStatus> { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 197 | let status: Result<RepoStatus> = |
| 198 | g1t_kit::call(&self.repos, "status_by_id", &StatusByIdArgs { id: repo_id.to_owned() }).await; | |
| 199 | Ok(match status { | |
| A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so. | 200 | Ok(status) => status, |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 201 | Err(error) => { |
| 202 | worker::console_error!("security: status_by_id {repo_id}: {error}"); | |
| A desktop release goes out with what can be built. Linux is built on g1t's machines in every release; Windows and macOS each wait on a repository variable: a machine registered for them, or for Windows a cross build on Linux once aka.ms and download.visualstudio.microsoft.com are workflow-only domains, which is where the first release stopped. A platform with no machine is skipped and the download page says it is coming, with a word for the visitor whose platform this release lacks. The guide and CONTRIBUTING say so. | 203 | RepoStatus::default() |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 204 | } |
| 205 | }) | |
| 206 | } | |
| 207 | ||
| 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. | 208 | /// Whether the repository is neither archived nor deleted. Scanning |
| 209 | /// goes on in a mirror: it reads. | |
| 210 | async fn active(&self, repo_id: &str) -> Result<bool> { | |
| 211 | Ok(self.status(repo_id).await?.active()) | |
| 212 | } | |
| 213 | ||
| 214 | /// Whether g1t may open branches and pull requests here on its own | |
| 215 | /// (security and dependency updates): active, and not a mirror short | |
| 216 | /// of a takeover (`g1t_contracts::mirrors`). `Err` says why not. | |
| 217 | async fn takes_updates(&self, repo: &RepoRow) -> Result<std::result::Result<(), String>> { | |
| 218 | let status = self.status(&repo.repo_id).await?; | |
| 219 | if status.takes_updates() { | |
| 220 | return Ok(Ok(())); | |
| 221 | } | |
| 222 | Ok(Err(match &status.mirror { | |
| 223 | Some(mirror) if status.active() => g1t_contracts::mirrors::updates_wait_message(&repo.namespace, &repo.name, mirror), | |
| 224 | _ => "Updates wait while the repository is archived or deleted.".to_owned(), | |
| 225 | })) | |
| 226 | } | |
| 227 | ||
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 228 | /// Records a repository named in an event, by id. Forks are not |
| 229 | /// recorded: a pull request's findings belong to its repository. | |
| 230 | async fn register_by_id(&self, repo_id: &str) -> Result<Option<RepoRow>> { | |
| 231 | if let Some(row) = self.store.repo(repo_id).await? { | |
| 232 | return Ok(Some(row)); | |
| 233 | } | |
| 234 | let path: Option<RepoPath> = g1t_kit::call(&self.repos, "path_by_id", &PathByIdArgs { id: repo_id.to_owned() }).await?; | |
| 235 | match path { | |
| 236 | Some(path) => Ok(Some(self.store.register(repo_id, &path.namespace, &path.name).await?)), | |
| 237 | None => Ok(None), | |
| 238 | } | |
| 239 | } | |
| 240 | ||
| 241 | async fn overview(&self, a: OverviewArgs) -> Result<Outcome<SecurityOverview>> { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 242 | let mut repo = match self.member_repo(&a.repo, &a.viewer, SEE_FINDINGS).await? { |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 243 | Outcome::Ok(repo) => repo, |
| 244 | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 245 | }; | |
| 246 | // The first look at a repository reads its dependencies at once; | |
| 247 | // its history is scanned in the background. | |
| 248 | if repo.deps_scanned_at.is_none() { | |
| 249 | self.scan_dependencies(&repo).await?; | |
| 250 | repo = self.store.repo(&repo.repo_id).await?.unwrap_or(repo); | |
| 251 | } | |
| 252 | let (counts, _, _) = self.store.counts(&repo.repo_id).await?; | |
| 253 | Ok(Outcome::Ok(SecurityOverview { | |
| 254 | repo_id: repo.repo_id.clone(), | |
| 255 | counts, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 256 | secret_counts: self.store.secret_counts(&repo.repo_id).await?, |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 257 | secrets: self.store.secrets(&repo.repo_id).await?, |
| 258 | vulnerabilities: self.store.vulnerabilities(&repo.repo_id).await?, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 259 | activity: self.store.activity(&repo.repo_id, ACTIVITY_SHOWN).await?, |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 260 | scan: repo.scan_state(), |
| 261 | upkeep: repo.upkeep != 0, | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 262 | version_updates: self.version_updates_view(&repo).await?, |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 263 | })) |
| 264 | } | |
| 265 | ||
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 266 | /// What dismissing or reopening alert `id` takes: Write, for a secret |
| 267 | /// as for a vulnerable dependency, as on GitHub; a security manager may | |
| 268 | /// too. | |
| 269 | fn capability_for(_id: &str) -> Capability { | |
| 270 | SEE_FINDINGS | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 271 | } |
| 272 | ||
| 273 | async fn dismiss(&self, a: DismissArgs) -> Result<Outcome<AlertChange>> { | |
| 274 | let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Self::capability_for(&a.id)).await? { | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 275 | Outcome::Ok(repo) => repo, |
| 276 | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 277 | }; | |
| 278 | if !a.actor.verified { | |
| 279 | return Ok(fail(FailureCode::Forbidden, "Confirm your email address first.")); | |
| 280 | } | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 281 | let comment: String = a.comment.trim().chars().take(MAX_COMMENT_CHARS).collect(); |
| 282 | let comment = (!comment.is_empty()).then_some(comment); | |
| 283 | if let Some(finding) = self.store.secret(&repo.repo_id, &a.id).await? { | |
| 284 | if !a.reason.for_secrets() { | |
| 285 | return Ok(fail( | |
| 286 | FailureCode::Invalid, | |
| 287 | "A secret is dismissed as false_positive, used_in_tests, revoked or wont_fix.", | |
| 288 | )); | |
| 289 | } | |
| 290 | if finding.state != AlertState::Open { | |
| 291 | return Ok(fail(FailureCode::Conflict, "This alert is not open. Reopen it first to dismiss it again.")); | |
| 292 | } | |
| 293 | self.store | |
| 294 | .dismiss_secret(&repo.repo_id, &a.id, a.reason, &a.actor.username, comment.as_deref()) | |
| 295 | .await?; | |
| 296 | self.store | |
| 297 | .record(&repo.repo_id, &[Activity { | |
| 298 | alert_id: &a.id, | |
| 299 | action: "dismissed", | |
| 300 | actor: Some(&a.actor.username), | |
| 301 | reason: Some(a.reason), | |
| 302 | comment: comment.as_deref(), | |
| 303 | number: None, | |
| 304 | }]) | |
| 305 | .await?; | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 306 | let secret = self.store.secret(&repo.repo_id, &a.id).await?; |
| 307 | if let Some(secret) = &secret { | |
| 308 | // Revoked is fixed; any other reason, dismissed. | |
| 309 | let action = if a.reason == DismissReason::Revoked { "fixed" } else { "dismissed" }; | |
| 310 | let event = g1t_contracts::security_suite::SecurityEvent { | |
| 311 | reason: Some(a.reason.as_str().to_owned()), | |
| 312 | ..secret_alerts::secret_event(&repo, secret) | |
| 313 | }; | |
| 314 | self.alert_event(g1t_contracts::security_suite::AlertType::SecretScanning, action, &repo, event, Some(a.actor.id.clone())) | |
| 315 | .await; | |
| 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. | 316 | let changed = summaries::Changed { |
| 317 | scan: "change", | |
| 318 | fixed: if action == "fixed" { vec![a.id.clone()] } else { Vec::new() }, | |
| 319 | dismissed: u32::from(action == "dismissed"), | |
| 320 | ..summaries::Changed::default() | |
| 321 | }; | |
| 322 | self.summarize(&repo, g1t_contracts::security_suite::AlertType::SecretScanning, changed, Some(a.actor.id.clone())).await?; | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 323 | } |
| 324 | return Ok(Outcome::Ok(AlertChange { secret, vulnerability: None })); | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 325 | } |
| 326 | let Some(vuln) = self.store.vulnerability(&repo.repo_id, &a.id).await? else { | |
| 327 | return Ok(fail(FailureCode::NotFound, "No such alert.")); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 328 | }; |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 329 | if a.reason.for_secrets() { |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 330 | return Ok(fail( |
| 331 | FailureCode::Invalid, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 332 | "A dependency is dismissed as fix_started, no_bandwidth, tolerable_risk, inaccurate or not_used.", |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 333 | )); |
| 334 | } | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 335 | if vuln.state != AlertState::Open { |
| 336 | return Ok(fail(FailureCode::Conflict, "This alert is not open. Reopen it first to dismiss it again.")); | |
| 337 | } | |
| 338 | self.store | |
| 339 | .dismiss_vulnerability(&repo.repo_id, &a.id, a.reason, &a.actor.username, comment.as_deref()) | |
| 340 | .await?; | |
| 341 | self.store | |
| 342 | .record(&repo.repo_id, &[Activity { | |
| 343 | alert_id: &a.id, | |
| 344 | action: "dismissed", | |
| 345 | actor: Some(&a.actor.username), | |
| 346 | reason: Some(a.reason), | |
| 347 | comment: comment.as_deref(), | |
| 348 | number: None, | |
| 349 | }]) | |
| 350 | .await?; | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 351 | let vulnerability = self.store.vulnerability(&repo.repo_id, &a.id).await?; |
| 352 | if let Some(vuln) = &vulnerability { | |
| 353 | let event = g1t_contracts::security_suite::SecurityEvent { reason: Some(a.reason.as_str().to_owned()), ..deps::vulnerability_event(&repo, vuln) }; | |
| 354 | self.alert_event(g1t_contracts::security_suite::AlertType::Vulnerability, "dismissed", &repo, event, Some(a.actor.id.clone())).await; | |
| 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. | 355 | let changed = summaries::Changed { scan: "change", dismissed: 1, ..summaries::Changed::default() }; |
| 356 | self.summarize(&repo, g1t_contracts::security_suite::AlertType::Vulnerability, changed, Some(a.actor.id.clone())).await?; | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 357 | } |
| Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches | 358 | // A security update whose alerts are now all fixed or dismissed closes. |
| 359 | if let Err(error) = self.resolve_updates(&repo, None).await { | |
| 360 | worker::console_error!("security: updates of {} not resolved after a dismissal: {error}", repo.repo_id); | |
| 361 | } | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 362 | Ok(Outcome::Ok(AlertChange { secret: None, vulnerability })) |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 363 | } |
| 364 | ||
| 365 | async fn reopen(&self, a: ReopenArgs) -> Result<Outcome<AlertChange>> { | |
| 366 | let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Self::capability_for(&a.id)).await? { | |
| 367 | Outcome::Ok(repo) => repo, | |
| 368 | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 369 | }; |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 370 | if !a.actor.verified { |
| 371 | return Ok(fail(FailureCode::Forbidden, "Confirm your email address first.")); | |
| 372 | } | |
| 373 | let reopened = Activity { alert_id: &a.id, action: "reopened", actor: Some(&a.actor.username), reason: None, comment: None, number: None }; | |
| 374 | if let Some(finding) = self.store.secret(&repo.repo_id, &a.id).await? { | |
| 375 | if finding.state == AlertState::Open { | |
| 376 | return Ok(fail(FailureCode::Conflict, "This alert is already open.")); | |
| 377 | } | |
| 378 | // A secret that never landed has nothing to reopen to but blocked. | |
| 379 | let status = if finding.source == "push" && finding.test_value.is_none() { SecretStatus::Blocked } else { SecretStatus::Open }; | |
| 380 | self.store.reopen_secret(&repo.repo_id, &a.id, status).await?; | |
| 381 | self.store.record(&repo.repo_id, &[reopened]).await?; | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 382 | let secret = self.store.secret(&repo.repo_id, &a.id).await?; |
| 383 | if let Some(secret) = &secret { | |
| 384 | self.alert_event( | |
| 385 | g1t_contracts::security_suite::AlertType::SecretScanning, | |
| 386 | "reopened", | |
| 387 | &repo, | |
| 388 | secret_alerts::secret_event(&repo, secret), | |
| 389 | Some(a.actor.id.clone()), | |
| 390 | ) | |
| 391 | .await; | |
| 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. | 392 | // The thread's counts move; nobody is told anew of what they reopened. |
| 393 | let changed = summaries::Changed { scan: "change", ..summaries::Changed::default() }; | |
| 394 | self.summarize(&repo, g1t_contracts::security_suite::AlertType::SecretScanning, changed, Some(a.actor.id.clone())).await?; | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 395 | } |
| 396 | return Ok(Outcome::Ok(AlertChange { secret, vulnerability: None })); | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 397 | } |
| 398 | let Some(vuln) = self.store.vulnerability(&repo.repo_id, &a.id).await? else { | |
| 399 | return Ok(fail(FailureCode::NotFound, "No such alert.")); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 400 | }; |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 401 | if vuln.state != AlertState::Dismissed { |
| 402 | return Ok(fail(FailureCode::Conflict, "Only a dismissed alert can be reopened; a fixed one reopens when it is found again.")); | |
| 403 | } | |
| 404 | self.store.reopen_vulnerability(&repo.repo_id, &a.id).await?; | |
| 405 | self.store.record(&repo.repo_id, &[reopened]).await?; | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 406 | let vulnerability = self.store.vulnerability(&repo.repo_id, &a.id).await?; |
| 407 | if let Some(vuln) = &vulnerability { | |
| 408 | self.alert_event( | |
| 409 | g1t_contracts::security_suite::AlertType::Vulnerability, | |
| 410 | "reopened", | |
| 411 | &repo, | |
| 412 | deps::vulnerability_event(&repo, vuln), | |
| 413 | Some(a.actor.id.clone()), | |
| 414 | ) | |
| 415 | .await; | |
| 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. | 416 | let changed = summaries::Changed { scan: "change", ..summaries::Changed::default() }; |
| 417 | self.summarize(&repo, g1t_contracts::security_suite::AlertType::Vulnerability, changed, Some(a.actor.id.clone())).await?; | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 418 | } |
| 419 | Ok(Outcome::Ok(AlertChange { secret: None, vulnerability })) | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 420 | } |
| 421 | ||
| 422 | async fn rescan(&self, a: RescanArgs) -> Result<Outcome<ScanState>> { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 423 | let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), SEE_FINDINGS).await? { |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 424 | Outcome::Ok(repo) => repo, |
| 425 | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 426 | }; | |
| 427 | self.store.restart_history(&repo.repo_id).await?; | |
| 428 | self.scan_dependencies(&repo).await?; | |
| 429 | if let Some(fresh) = self.store.repo(&repo.repo_id).await? { | |
| 430 | self.advance_history(&fresh, 1).await?; | |
| 431 | } | |
| 432 | let state = self.store.repo(&repo.repo_id).await?.map(|row| row.scan_state()).unwrap_or_default(); | |
| 433 | Ok(Outcome::Ok(state)) | |
| 434 | } | |
| 435 | ||
| 436 | async fn set_upkeep(&self, a: SetUpkeepArgs) -> Result<Outcome<bool>> { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 437 | // Whether agents keep its dependencies up to date is one of its |
| 438 | // settings. | |
| 439 | let repo = match self.member_repo(&a.repo, &Some(a.actor.clone()), Capability::ManageSettings).await? { | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 440 | Outcome::Ok(repo) => repo, |
| 441 | Outcome::Fail(failure) => return Ok(Outcome::Fail(failure)), | |
| 442 | }; | |
| 443 | if !a.actor.verified { | |
| 444 | return Ok(fail(FailureCode::Forbidden, "Confirm your email address first.")); | |
| 445 | } | |
| 446 | self.store.set_upkeep(&repo.repo_id, a.enabled, &a.actor.username).await?; | |
| 447 | Ok(Outcome::Ok(a.enabled)) | |
| 448 | } | |
| 449 | ||
| 450 | async fn workspace(&self, a: WorkspaceArgs) -> Result<Outcome<Vec<RepoSecurity>>> { | |
| 451 | let workspace = a.workspace.to_lowercase(); | |
| 452 | if !a.viewer.as_ref().is_some_and(|user| user.is_member(&workspace)) { | |
| 453 | return Ok(fail(FailureCode::NotFound, "Workspace not found.")); | |
| 454 | } | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 455 | // Only the repositories whose findings the viewer may see. The |
| 456 | // Write role is never had through being public, so treating each | |
| 457 | // as private changes nothing. | |
| 458 | let repos: Vec<RepoRow> = self | |
| 459 | .store | |
| 460 | .in_namespace(&workspace) | |
| 461 | .await? | |
| 462 | .into_iter() | |
| 463 | .filter(|repo| { | |
| 464 | let target = access::RepoRef { id: &repo.repo_id, namespace: &workspace, private: true }; | |
| 465 | access::can(a.viewer.as_ref(), target, SEE_FINDINGS) | |
| 466 | }) | |
| 467 | .collect(); | |
| 468 | // Every repository's counts at once, not one after another. | |
| 469 | let counts = futures_util::future::try_join_all(repos.iter().map(|repo| self.store.counts(&repo.repo_id))).await?; | |
| 470 | let list = repos | |
| 471 | .into_iter() | |
| 472 | .zip(counts) | |
| 473 | .map(|(repo, (counts, secrets, vulnerabilities))| RepoSecurity { | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 474 | repo_id: repo.repo_id, |
| 475 | name: repo.name, | |
| 476 | counts, | |
| 477 | secrets, | |
| 478 | vulnerabilities, | |
| 479 | upkeep: repo.upkeep != 0, | |
| 480 | dependencies_scanned_at: repo.deps_scanned_at, | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 481 | }) |
| 482 | .collect(); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 483 | Ok(Outcome::Ok(list)) |
| 484 | } | |
| 485 | ||
| 486 | /// Push protection's question: which of these secrets were allowed? | |
| 487 | /// The others are recorded as blocked, so someone can allow them. | |
| 488 | async fn push_blocked(&self, a: PushBlockedArgs) -> Result<PushVerdict> { | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 489 | let repo = self.store.register(&a.repo_id, &a.path.namespace, &a.path.name).await?; |
| 490 | if let Some(private) = a.private { | |
| 491 | self.store.set_private(&a.repo_id, private).await?; | |
| 492 | } | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 493 | let fingerprints: Vec<String> = a.secrets.iter().map(|secret| secret.fingerprint.clone()).collect(); |
| 494 | let known = self.store.known(&a.repo_id, &fingerprints).await?; | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 495 | let mut allowed = let_through(&known, &a.secrets); |
| 496 | // Bypassed with a reason: let through, whatever the alert says now. | |
| 497 | allowed.extend(self.store.bypassed(&a.repo_id, &fingerprints).await?); | |
| 498 | allowed.sort(); | |
| 499 | allowed.dedup(); | |
| 500 | let all = a.secrets.clone(); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 501 | let fresh: Vec<NewSecret> = a |
| 502 | .secrets | |
| 503 | .into_iter() | |
| 504 | .filter(|secret| !known.iter().any(|(fingerprint, _, _)| *fingerprint == secret.fingerprint)) | |
| 505 | .collect(); | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 506 | // A likely test value goes through, so it lands: open, not blocked. |
| 507 | let (tests, real): (Vec<NewSecret>, Vec<NewSecret>) = fresh.into_iter().partition(|secret| secret.test_value.is_some()); | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 508 | self.store |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 509 | .add_secrets(&a.repo_id, &real, SecretStatus::Blocked, "push", a.pusher.as_deref()) |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 510 | .await?; |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 511 | self.store |
| 512 | .add_secrets(&a.repo_id, &tests, SecretStatus::Open, "push", a.pusher.as_deref()) | |
| 513 | .await?; | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 514 | let fresh: Vec<String> = real.iter().map(|secret| secret.fingerprint.clone()).collect(); |
| 515 | self.secrets_found(&repo, &all, "push", &fresh, a.pusher.as_deref()).await?; | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 516 | let ids = self |
| 517 | .store | |
| 518 | .known(&a.repo_id, &fingerprints) | |
| 519 | .await? | |
| 520 | .into_iter() | |
| 521 | .filter(|(fingerprint, _, _)| !allowed.contains(fingerprint)) | |
| 522 | .map(|(fingerprint, id, _)| (fingerprint, id)) | |
| 523 | .collect(); | |
| 524 | Ok(PushVerdict { allowed, ids }) | |
| 525 | } | |
| 526 | ||
| 527 | /// What happens on the bus that concerns this service. | |
| 528 | async fn on_event(&self, event: &Event) -> Result<()> { | |
| 529 | match event.kind.as_str() { | |
| 530 | "git.push" => { | |
| 531 | let Ok(pushed) = serde_json::from_value::<Pushed>(event.data.clone()) else { | |
| 532 | return Ok(()); | |
| 533 | }; | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 534 | // Too large to scan before it was stored: its new commits |
| 535 | // are scanned now, on whichever branch. | |
| 536 | if pushed.unscanned | |
| 537 | && !pushed.after.is_empty() | |
| 538 | && let Some(repo) = self.register_by_id(&pushed.repo_id).await? | |
| 539 | { | |
| 540 | let id = self | |
| 541 | .store | |
| 542 | .add_push_scan(&repo.repo_id, &pushed.git_ref, &pushed.after, pushed.before.as_deref(), event.actor.as_deref()) | |
| 543 | .await?; | |
| 544 | self.advance_push_scan(&id, history::PUSH_PAGES_AT_ONCE).await?; | |
| 545 | } | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 546 | // A version update's branch, or a grouped security update's, |
| 547 | // pushed by its sandbox: time for its pull request. | |
| 548 | if !pushed.default_branch | |
| 549 | && let Some(branch) = pushed.git_ref.strip_prefix("refs/heads/") | |
| 550 | && self.update_pull_pushed(&pushed.repo_id, branch, &pushed.after).await? | |
| 551 | { | |
| 552 | return Ok(()); | |
| 553 | } | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 554 | // A security update's branch, pushed by its sandbox: time for |
| 555 | // its pull request. | |
| 556 | if let Some(branch) = pushed.git_ref.strip_prefix("refs/heads/") | |
| 557 | && branch.starts_with(UPDATE_BRANCH_PREFIX) | |
| 558 | { | |
| Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches | 559 | self.update_pushed(&pushed.repo_id, branch, &pushed.after).await?; |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 560 | return Ok(()); |
| 561 | } | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 562 | if !pushed.default_branch { |
| 563 | return Ok(()); | |
| 564 | } | |
| 565 | if let Some(repo) = self.register_by_id(&pushed.repo_id).await? { | |
| 566 | self.scan_dependencies(&repo).await?; | |
| 567 | if repo.history != "done" { | |
| 568 | self.advance_history(&repo, 1).await?; | |
| 569 | } | |
| 570 | } | |
| 571 | } | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 572 | "pull.merged" | "pull.closed" | "checks.completed" => { |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 573 | if let Ok(happened) = serde_json::from_value::<PullHappened>(event.data.clone()) |
| 574 | && !(event.kind != "checks.completed" && self.update_pull_closed(&event.kind, &happened.repo_id, happened.number).await?) | |
| 575 | { | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 576 | self.update_pull_event( |
| 577 | &event.kind, | |
| 578 | &happened.repo_id, | |
| 579 | happened.number, | |
| 580 | happened.status.as_deref(), | |
| 581 | event.actor.as_deref(), | |
| 582 | ) | |
| 583 | .await?; | |
| 584 | } | |
| Merge update PRs close themselves: g1t closes its security and version updates once they are no longer needed, and deletes their branches | 585 | // One of g1t's update pull requests closed or merged, by g1t |
| 586 | // or by a person: its branch goes. | |
| 587 | if event.kind != "checks.completed" | |
| 588 | && let Ok(happened) = serde_json::from_value::<PullHappened>(event.data.clone()) | |
| 589 | { | |
| 590 | self.pull_finished(&happened.repo_id, happened.number).await?; | |
| 591 | } | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 592 | } |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 593 | "comment.created" => self.update_comment(event).await?, |
| 594 | // A pull request opened or its head moved: a dependency update | |
| 595 | // file it changes is checked (not on `pull.ready`, which moves | |
| 596 | // nothing), and dependency review runs. | |
| 597 | "pull.opened" | "pull.updated" | "pull.ready" => { | |
| 598 | if event.kind != "pull.ready" { | |
| 599 | self.check_dependabot_file(event).await?; | |
| 600 | } | |
| 601 | if let Ok(happened) = serde_json::from_value::<PullHappened>(event.data.clone()) { | |
| 602 | self.review_pull(&happened.repo_id, happened.number).await?; | |
| 603 | } | |
| 604 | } | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 605 | "repo.created" => { |
| 606 | if let Ok(created) = serde_json::from_value::<Created>(event.data.clone()) { | |
| Usage, Billing settings and prepaid AI credit; fixes from the UX audit | 607 | // Recorded with its visibility, so the overview never takes a |
| 608 | // public repository for a private one. | |
| 609 | if self.register_by_id(&created.repo_id).await?.is_some() | |
| 610 | && let Some(private) = created.is_private | |
| 611 | { | |
| 612 | self.store.set_private(&created.repo_id, private).await?; | |
| 613 | } | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 614 | } |
| 615 | } | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 616 | "repo.visibility_changed" => { |
| 617 | if let Ok(changed) = serde_json::from_value::<g1t_contracts::events::RepoVisibilityChanged>(event.data.clone()) | |
| 618 | && self.store.repo(&changed.repo_id).await?.is_some() | |
| 619 | { | |
| 620 | self.store.set_private(&changed.repo_id, changed.is_private).await?; | |
| 621 | } | |
| 622 | } | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 623 | // A repository transferred or renamed: it is recorded under its new path. |
| 624 | "repo.transferred" | "repo.renamed" => { | |
| 625 | if let Some(moved) = g1t_kit::transfer::read(event) { | |
| 626 | // Where it is now, so moves heard out of order end in | |
| 627 | // the same place. | |
| 628 | let now: Option<g1t_contracts::repos::RepoPath> = g1t_kit::call( | |
| 629 | &self.repos, | |
| 630 | "path_by_id", | |
| 631 | &g1t_contracts::repos::PathByIdArgs { id: moved.repo_id.clone() }, | |
| 632 | ) | |
| 633 | .await?; | |
| 634 | let current = now.map_or_else( | |
| 635 | || moved.destination().to_owned(), | |
| 636 | |path| format!("{}/{}", path.namespace, path.name), | |
| 637 | ); | |
| 638 | if let Some((namespace, name)) = current.split_once('/') { | |
| 639 | self.store.moved(&moved.repo_id, namespace, name).await?; | |
| 640 | } | |
| 641 | } | |
| 642 | } | |
| 643 | // A repository purged: everything found in it goes. | |
| 644 | "repo.purged" => { | |
| 645 | if let Some(g1t_kit::lifecycle::Lifecycle::Purged(purged)) = g1t_kit::lifecycle::read(event) { | |
| 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. | 646 | self.store.purge_advisories(&purged.repo_id).await?; |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 647 | self.store.purge_suite(&purged.repo_id).await?; |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 648 | self.store.purge(&purged.repo_id).await?; |
| 649 | } | |
| 650 | } | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 651 | "workspace.renamed" => { |
| 652 | if let Ok(renamed) = serde_json::from_value::<WorkspaceRenamed>(event.data.clone()) { | |
| 653 | self.store.rename_namespace(&renamed.stale_slugs(&renamed.to), &renamed.to).await?; | |
| 654 | } | |
| 655 | } | |
| 656 | _ => {} | |
| 657 | } | |
| 658 | Ok(()) | |
| 659 | } | |
| 660 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 661 | /// The sweep: continues history scans and scans of pushes that landed |
| 662 | /// unscanned, catches security updates whose sandbox never pushed, and | |
| 663 | /// reads dependencies that have not been read for a day. | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 664 | async fn sweep(&self) -> Result<()> { |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 665 | for scan in self.store.pending_push_scans(HISTORIES_PER_SWEEP).await? { |
| 666 | if let Err(error) = self.advance_push_scan(&scan.id, PAGES_PER_SWEEP).await { | |
| 667 | worker::console_error!("security: push scan {} not continued: {error}", scan.id); | |
| 668 | } | |
| 669 | } | |
| 670 | if let Err(error) = self.stalled_updates().await { | |
| 671 | worker::console_error!("security: stalled security updates not handled: {error}"); | |
| 672 | } | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 673 | if let Err(error) = self.sweep_suite().await { |
| 674 | worker::console_error!("security: daily snapshots and validity checks: {error}"); | |
| 675 | } | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 676 | // Archived and deleted repositories wait; a few more are looked at |
| 677 | // so that they do not hold up the rest. | |
| 678 | let mut histories = 0; | |
| 679 | for repo in self.store.unfinished_histories(HISTORIES_PER_SWEEP * 4).await? { | |
| 680 | if histories == HISTORIES_PER_SWEEP { | |
| 681 | break; | |
| 682 | } | |
| 683 | if !self.active(&repo.repo_id).await? { | |
| 684 | continue; | |
| 685 | } | |
| 686 | histories += 1; | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 687 | if let Err(error) = self.advance_history(&repo, PAGES_PER_SWEEP).await { |
| 688 | worker::console_error!("security: history of {} not scanned: {error}", repo.repo_id); | |
| 689 | } | |
| 690 | } | |
| 691 | let day_ago = rfc3339(now_ms().saturating_sub(DAY_MS)); | |
| 692 | for repo in self.store.stale_dependencies(&day_ago, DEPENDENCIES_PER_SWEEP).await? { | |
| Invite-only launch: sign in with GitHub, repository access and lifecycle, many emails, a new look | 693 | if !self.active(&repo.repo_id).await? { |
| 694 | self.store | |
| 695 | .skip_dependencies(&repo.repo_id, "Dependencies are not checked while the repository is archived or deleted.") | |
| 696 | .await?; | |
| 697 | continue; | |
| 698 | } | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 699 | if let Err(error) = self.scan_dependencies(&repo).await { |
| 700 | worker::console_error!("security: dependencies of {} not read: {error}", repo.repo_id); | |
| 701 | } | |
| 702 | } | |
| 703 | Ok(()) | |
| 704 | } | |
| 705 | } | |
| 706 | ||
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 707 | /// The fingerprints a push may carry: those someone dismissed (allowed), |
| 708 | /// and likely test values, which are recorded but never stop a push. | |
| 709 | /// `known` is (fingerprint, id, status) of findings already recorded. | |
| 710 | fn let_through(known: &[(String, String, String)], secrets: &[NewSecret]) -> Vec<String> { | |
| 711 | let mut allowed: Vec<String> = known | |
| 712 | .iter() | |
| 713 | .filter(|(_, _, status)| status == "allowed") | |
| 714 | .map(|(fingerprint, _, _)| fingerprint.clone()) | |
| 715 | .chain(secrets.iter().filter(|secret| secret.test_value.is_some()).map(|secret| secret.fingerprint.clone())) | |
| 716 | .collect(); | |
| 717 | allowed.sort(); | |
| 718 | allowed.dedup(); | |
| 719 | allowed | |
| 720 | } | |
| 721 | ||
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 722 | #[event(fetch)] |
| 723 | async fn fetch(mut request: Request, env: Env, _ctx: Context) -> Result<Response> { | |
| 724 | let Some(method) = rpc_method(&request) else { | |
| 725 | return Response::error("Not found", 404); | |
| 726 | }; | |
| 727 | let security = Security::new(&env)?; | |
| 728 | let body: serde_json::Value = request.json().await?; | |
| 729 | match method.as_str() { | |
| 730 | "overview" => reply(&security.overview(args(body)?).await?), | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 731 | "dismiss" => reply(&security.dismiss(args(body)?).await?), |
| 732 | "reopen" => reply(&security.reopen(args(body)?).await?), | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 733 | "rescan" => reply(&security.rescan(args(body)?).await?), |
| 734 | "set_upkeep" => reply(&security.set_upkeep(args(body)?).await?), | |
| 735 | "workspace" => reply(&security.workspace(args(body)?).await?), | |
| 736 | "push_blocked" => reply(&security.push_blocked(args(body)?).await?), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 737 | "check_updates" => reply(&security.check_updates(args(body)?).await?), |
| 738 | // The security suite. | |
| 739 | "patterns_for" => reply(&security.patterns_for(args(body)?).await?), | |
| 740 | "custom_patterns" => reply(&security.custom_patterns(args(body)?).await?), | |
| 741 | "save_custom_pattern" => reply(&security.save_custom_pattern(args(body)?).await?), | |
| 742 | "delete_custom_pattern" => reply(&security.delete_custom_pattern(args(body)?).await?), | |
| 743 | "dry_run_pattern" => reply(&security.dry_run_pattern(args(body)?).await?), | |
| 744 | "secret_alert" => reply(&security.secret_alert(args(body)?).await?), | |
| 745 | "bypass" => reply(&security.bypass(args(body)?).await?), | |
| 746 | "bypass_requests" => reply(&security.bypass_requests(args(body)?).await?), | |
| 747 | "review_bypass" => reply(&security.review_bypass(args(body)?).await?), | |
| 748 | "check_validity" => reply(&security.check_validity(args(body)?).await?), | |
| 749 | "upload_sarif" => reply(&security.upload_sarif(args(body)?).await?), | |
| 750 | "sarif_status" => reply(&security.sarif_status(args(body)?).await?), | |
| 751 | "code_scanning" => reply(&security.code_scanning(args(body)?).await?), | |
| 752 | "code_alert" => reply(&security.code_alert(args(body)?).await?), | |
| 753 | "set_code_alert_state" => reply(&security.set_code_alert_state(args(body)?).await?), | |
| 754 | "pull_code_scanning" => reply(&security.pull_code_scanning(args(body)?).await?), | |
| 755 | "fix_alert" => reply(&security.fix_alert(args(body)?).await?), | |
| 756 | "dependency_graph" => reply(&security.dependency_graph(args(body)?).await?), | |
| 757 | "sbom" => reply(&security.sbom(args(body)?).await?), | |
| 758 | "dependency_review" => reply(&security.dependency_review(args(body)?).await?), | |
| 759 | "security_settings" => reply(&security.security_settings(args(body)?).await?), | |
| 760 | "set_security_settings" => reply(&security.set_security_settings(args(body)?).await?), | |
| 761 | "workspace_security_settings" => reply(&security.workspace_security_settings(args(body)?).await?), | |
| 762 | "set_workspace_security_settings" => reply(&security.set_workspace_security_settings(args(body)?).await?), | |
| 763 | "security_overview" => reply(&security.security_overview(args(body)?).await?), | |
| 764 | "workspace_alerts" => reply(&security.workspace_alerts(args(body)?).await?), | |
| 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. | 765 | // Reporting: advisories and private vulnerability reports (advisories.rs). |
| 766 | "list_advisories" => reply(&security.list_advisories(args(body)?).await?), | |
| 767 | "get_advisory" => reply(&security.get_advisory(args(body)?).await?), | |
| 768 | "create_advisory" => reply(&security.create_advisory(args(body)?).await?), | |
| 769 | "update_advisory" => reply(&security.update_advisory(args(body)?).await?), | |
| 770 | "set_advisory_state" => reply(&security.set_advisory_state(args(body)?).await?), | |
| 771 | "report_vulnerability" => reply(&security.report_vulnerability(args(body)?).await?), | |
| A repository's Security overview is one row per feature in three groups, each with a status word (Enabled, Disabled, Needs setup, Coming), a line on what it does and the one thing to do about it: the security policy (View policy, or Set up a security policy, which commits a starting SECURITY.md on a branch and opens it as a pull request), dependency alerts, security updates (the switch, kept on Vulnerabilities and now in Settings too), code scanning (Enabled once an analysis was uploaded), secret scanning (push protection always on) and code quality as Coming; and under Reporting, advisories and the private vulnerability reporting switch. Anyone who can see a public repository but has no role on it sees the public face at the same address: the policy, the published advisories and, when it is on, Report a vulnerability. Advisories get their pages (the list with its state filter, one advisory with Publish, Withdraw and Edit, a form for a new draft or changes) and reports theirs (new ones first, each accepted into a draft advisory or declined), with a Reports page beside Advisories and an Advisories tab; the public report form takes an email when signed out and counts against the front door's heavy limit. Security settings gain Alerts for secrets in the history and Private vulnerability reporting. Settings → Notifications gains a Security section: critical and high alerts at once (always, for the workspaces one owns or manages security for) and the rest in a daily or weekly digest, at once, or not at all, with security alerts now among the email reasons. The notifications guide says how security alerts arrive and what the digest is, the security guide the overview's rows and the public face, secret protection the history alerts switch, webhooks the summary and advisory events, and a new guide covers advisories and private reporting; the report_settings method lets the public page know whether a repository takes reports. | 772 | "report_settings" => reply(&security.report_settings(args(body)?).await?), |
| 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. | 773 | "list_vulnerability_reports" => reply(&security.list_vulnerability_reports(args(body)?).await?), |
| 774 | "review_vulnerability_report" => reply(&security.review_vulnerability_report(args(body)?).await?), | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 775 | _ => Response::error("Unknown method", 404), |
| 776 | } | |
| 777 | } | |
| 778 | ||
| 779 | #[event(queue)] | |
| 780 | async fn queue(batch: MessageBatch<Event>, env: Env, _ctx: Context) -> Result<()> { | |
| 781 | let security = Security::new(&env)?; | |
| 782 | for message in batch.messages()? { | |
| 783 | let event = message.body(); | |
| 784 | if let Err(error) = security.on_event(event).await { | |
| 785 | // Scans are idempotent and the sweep catches up, so one failed | |
| 786 | // event is logged rather than retried. | |
| 787 | worker::console_error!("security: {} {} failed: {error}", event.kind, event.id); | |
| 788 | } | |
| 789 | } | |
| 790 | Ok(()) | |
| 791 | } | |
| 792 | ||
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 793 | /// The cron (wrangler.jsonc) that runs version updates that are due. |
| 794 | const VERSION_UPDATES_CRON: &str = "*/5 * * * *"; | |
| 795 | ||
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 796 | #[event(scheduled)] |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 797 | async fn scheduled(event: ScheduledEvent, env: Env, _ctx: ScheduleContext) { |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 798 | match Security::new(&env) { |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 799 | // Version updates run every few minutes, so a schedule's time is kept. |
| 800 | Ok(security) if event.cron() == VERSION_UPDATES_CRON => { | |
| 801 | if let Err(error) = security.version_update_sweep().await { | |
| 802 | worker::console_error!("security: the version update sweep failed: {error}"); | |
| 803 | } | |
| 804 | } | |
| Agents get guardrails, run credentials, an audit log, a context hub, repository instructions and mentions; security upkeep; snake_case API | 805 | Ok(security) => { |
| 806 | if let Err(error) = security.sweep().await { | |
| 807 | worker::console_error!("security: the sweep failed: {error}"); | |
| 808 | } | |
| 809 | } | |
| 810 | Err(error) => worker::console_error!("security: could not start: {error}"), | |
| 811 | } | |
| 812 | } | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 813 | |
| 814 | #[cfg(test)] | |
| 815 | mod tests { | |
| 816 | use super::*; | |
| 817 | ||
| 818 | fn secret(fingerprint: &str, test_value: Option<&str>) -> NewSecret { | |
| 819 | NewSecret { | |
| 820 | fingerprint: fingerprint.into(), | |
| 821 | kind: "aws_access_key".into(), | |
| 822 | path: "a.env".into(), | |
| 823 | line: 1, | |
| 824 | commit: "c".into(), | |
| 825 | preview: "AKIA…".into(), | |
| 826 | test_value: test_value.map(str::to_owned), | |
| Teams and CODEOWNERS, labels and milestones, dependency updates, the security suite, and a clearer top bar | 827 | pattern_id: None, |
| 828 | pattern_name: None, | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 829 | } |
| 830 | } | |
| 831 | ||
| 832 | #[test] | |
| 833 | fn dismissed_secrets_and_test_values_go_through() { | |
| 834 | let known = vec![ | |
| 835 | ("allowed".to_owned(), "sec_1".to_owned(), "allowed".to_owned()), | |
| 836 | ("resolved".to_owned(), "sec_2".to_owned(), "resolved".to_owned()), | |
| 837 | ("blocked".to_owned(), "sec_3".to_owned(), "blocked".to_owned()), | |
| 838 | ]; | |
| 839 | let secrets = [secret("allowed", None), secret("resolved", None), secret("example", Some("it says it is an example")), secret("real", None)]; | |
| 840 | assert_eq!(let_through(&known, &secrets), ["allowed", "example"]); | |
| 841 | } | |
| 842 | ||
| 843 | #[test] | |
| 844 | fn a_push_says_when_it_landed_unscanned() { | |
| 845 | let pushed: Pushed = serde_json::from_value(serde_json::json!({ | |
| 846 | "repoId": "rep_1", "ref": "refs/heads/import", "after": "abc", "defaultBranch": false, "unscanned": true | |
| 847 | })) | |
| 848 | .unwrap(); | |
| 849 | assert!(pushed.unscanned && pushed.before.is_none() && pushed.git_ref == "refs/heads/import"); | |
| 850 | let ordinary: Pushed = serde_json::from_value(serde_json::json!({ "repoId": "rep_1", "ref": "refs/heads/main", "after": "abc", "defaultBranch": true })).unwrap(); | |
| 851 | assert!(!ordinary.unscanned); | |
| 852 | } | |
| 853 | ||
| 854 | #[test] | |
| 855 | fn owners_are_told_what_landed_and_what_to_do() { | |
| 856 | let one = history::landed_secrets_intro("acme", "rocket", "import", 1); | |
| 857 | assert!(one.contains("A push to import in acme/rocket") && one.contains("a secret that looks real") && one.contains("Rotate")); | |
| 858 | assert!(history::landed_secrets_intro("acme", "rocket", "main", 3).contains("3 secrets that look real")); | |
| 859 | } | |
| 860 | ||
| 861 | #[test] | |
| 862 | fn dismissing_a_secret_takes_admin_and_a_dependency_write() { | |
| Merge main (membership, two-factor, GitHub repo roles) into tokens | 863 | assert_eq!(Security::capability_for("sec_1"), Capability::SecurityAlerts); |
| 864 | assert_eq!(Security::capability_for("vul_1"), Capability::SecurityAlerts); | |
| Git storage hardened, pages in tens of milliseconds, honest security alerts, and costs reconciled daily | 865 | } |
| 866 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.