Conversation
install.ps1 exits before the discovery agent starts when Python is missing or the repository cannot be downloaded. The agent is what reports to the backend, so those devices produce no device_discovery row, no device_scans row and no Sentry event. They are indistinguishable from a machine that was never enrolled. Git was a hard Windows prerequisite from 4 Feb until #262 yesterday. It printed "Git is not installed." to a console nobody reads, and the only symptom was 69 Xome devices that had checked in via MDM and had no discovery row, with nothing to explain them. Python is now the remaining gate of that shape. Send-InstallerFailure posts the existing scan lifecycle event (scan_event=failed) to /api/v1/ai-tools/report/, so the backend creates the device and scan rows it already knows how to create and the failure becomes a SQL question. No backend change. The script has already made an authenticated call to this host by that point, for the branch lookup, so network and credentials are proven. Uses Invoke-RestMethod for the Windows certificate store, matching the branch lookup above it and the archive download in #262; curl is the equivalent rule on the Python side. Swallows every error and caps at 10s: this runs on an already-broken install and must not change the exit code or add delay. Reports codes and versions, never usernames or hostnames. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
🛡️ Automated Security Review (consensus)
5 findings — 1 high-confidence, 4 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.
🔴 HIGH — API key sent over cleartext HTTP
install.ps1:67
- Impact: When
Domainis explicitlyhttp://, the credentialed failure-report POST sendsAuthorization: Bearerwithout encryption; the branch lookup above already blocks non-HTTPS, so this is the only path that can leak the org API key on the wire. - Fix: Gate the POST on
$_domain -match '^https://'(or normalize to HTTPS) before attaching the Bearer header — match the branch-lookup guard. - Flagged by: Claude, Cursor (Greptile inline)
🟡 TRIAGE — Download exception text may carry PII into scan_error
install.ps1:129 → install.ps1:218
- Impact:
$script:LastDownloadError = $_.Exception.Messageis forwarded asscan_error.message; .NET web/IO exceptions often embed full URLs (incl. query tokens), temp paths (C:\Users\<user>\...), and proxy hostnames — stored indevice_scanscontrary to the PR's "no PII" posture;COMPUTERNAMEfallback is also a hostname. - Fix: Send a bounded, sanitized message (fixed reason text + exception type, or strip URLs/paths and truncate); document hostname fallback if intentional.
- Flagged by: Claude, Cursor
🟡 TRIAGE — Placeholder BIOS serial can mis-attribute devices
install.ps1:51
- Impact: Nonblank placeholder serials (e.g.
To be filled by O.E.M.,0) are accepted asdevice_idwhile the normal agent rejects them and falls back to hostname — multiple machines can collapse under one ID or diverge from later scan identity. - Fix: Reuse the agent's serial validation / hostname fallback before posting.
- Flagged by: Cursor (Greptile inline)
🟡 TRIAGE — TLS protocol set widened with -bor instead of assigned
install.ps1:64
- Impact:
-bor Tls12keeps weaker protocols already enabled in the process; an active MITM could negotiate a downgrade on this credentialed POST (low practical risk on modern Windows defaults). - Fix: Assign explicitly, e.g.
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12(add Tls13 where supported). - Flagged by: Claude
🟡 TRIAGE — Bearer token may follow redirects cross-host
install.ps1:67
- Impact:
Invoke-RestMethodfollows redirects by default; on Windows PowerShell 5.1 theAuthorizationheader can be retained to a different host, forwarding the API key on misconfigured or hostile redirects. - Fix: Add
-MaximumRedirection 0to the report call (or validate final host matchesDomain). - Flagged by: Claude
Previously acknowledged (not re-flagged)
- Silent outer
catchon reporting — PR: fails silently by design; must not change exit code, raise a second error, or add delay on an already-broken install. - Undiagnosable reporting failures — same accepted tradeoff; operators cannot distinguish a recorded prerequisite failure from a failed report attempt.
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 49fdd5b6 · 2026-09-02T04:52Z
Two real defects from Greptile, both from not mirroring contracts that already exist a few lines away. The branch lookup at the top of the file guards on `-match '^https://'` before attaching the Bearer key, and normalization only prepends https when the domain has no scheme — an explicit `-Domain http://...` survives. The report attached the same key with no such guard, so an http domain would have put it on the wire in clear. Same guard now. Device id came straight from Win32_BIOS with no validation, while the agent runs it through is_valid_serial and falls back to the hostname. A machine reporting "To Be Filled By O.E.M." would have created one device_discovery row shared by every machine of that model, and it would not have matched the row a later successful scan writes. Get-DeviceId now applies the same INVALID_SERIAL_VALUES list with the same hostname fallback. Both covered by tests that fail when the guard or a list entry is removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
|
Both findings were real and are fixed in 61ba347. Neither was a judgement call — I failed to mirror contracts that already exist a few lines away in the same file. HTTPS guard. The branch lookup does exactly this before attaching the key: if ($_domain -and $_key -and $_domain -match '^https://') {and normalization only prepends https when the domain has no scheme, so an explicit if (-not $_key -or -not $_domain -or $_domain -notmatch '^https://') { return }Device identity. Worse than described, I think. I took Both are covered by tests that fail when the guard or any list entry is removed: The serial test imports Still worth a reviewer's eye on the PowerShell syntax — no |
Get-DeviceId dropped to the hostname after a single failed CIM query, while WindowsDeviceIdExtractor tries Get-WmiObject and two wmic forms first. A machine where CIM fails but WMI or wmic answers would file its installer failure under the hostname and its later scan under the BIOS serial, so the two would land on different device_discovery rows. That is the exact split this PR exists to prevent. Now walks the same four probes in the same order with the same validation, and only reaches COMPUTERNAME when all of them fail, which is where the agent lands too. Test asserts each probe is present and that they all precede the hostname fallback. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
|
Valid, fixed in f0adb82. I'd cut the chain to one probe. The agent runs four before giving up: Mine went CIM -> hostname. So on a box where CIM fails but Worth noting
The test also asserts every probe precedes the hostname fallback, so reordering breaks the build rather than silently splitting rows again. |
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
🛡️ Automated Security Review (consensus)
3 findings — 2 high-confidence, 1 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.
Raw exception text can persist paths/PII in scan_error
install.ps1:146, install.ps1:235 · 🔴 HIGH
Impact: $script:LastDownloadError = $_.Exception.Message is sent verbatim as scan_error.message; download/extraction errors often embed C:\Users\<username>\... paths, hostnames, or request URIs (including signed query params if the archive URL carries them).
Fix: Post a stable reason code or exception type only; if a message is needed, scrub \Users\ segments and query strings and cap length — add a test that rejects \Users\ in the payload.
Reviewers: Claude, Lead
Legacy TLS protocols remain enabled for authenticated requests
install.ps1:83 · 🔴 HIGH
Impact: SecurityProtocol -bor Tls12 adds TLS 1.2 but leaves SSL 3.0 / TLS 1.0 enabled process-wide, weakening negotiation for the branch lookup, archive download, and failure-report POST (all carry the Bearer key).
Fix: Assign [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 (optionally -bor Tls13 inside a try/catch on newer .NET) instead of OR-ing into the existing set.
Reviewers: Claude, Lead
Installer failure may report under a different device_id than the agent
install.ps1:51-58 · 🟡 TRIAGE
Impact: Get-DeviceId falls back to $env:COMPUTERNAME when CIM fails, but the agent can still recover the BIOS serial via WMIC — prerequisite failures and later scans may land on separate device rows.
Fix: Extend Get-DeviceId with the agent's WMIC fallback (or share one implementation) so failure reports target the same row as successful scans.
Reviewers: Greptile
Previously acknowledged (not re-flagged)
- Silent suppression of report errors (
install.ps1outercatch) — fails silently by design; must not alter exit code, surface secondary errors, or add delay on an already-broken install. - Bearer key sent over explicit
http://domain — fixed in61ba347; maintainer applied the same^https://guard used by the branch lookup. - Placeholder BIOS serial used as
device_id— fixed in61ba347;Get-DeviceIdnow mirrorsINVALID_SERIAL_VALUESand hostname fallback, with tests tied toconstants.py.
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 61ba3474 · 2026-09-02T05:02Z
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
I had added it ahead of the agent's own first probe. CIM and WMI are separate client stacks over the same class, so a machine where they disagree would have had its installer failure filed under the CIM answer and its scan under the WMI one. Adding a probe diverges just as much as omitting one. The three callers all launch install.ps1 through powershell.exe, never pwsh, so Get-WmiObject is always available and there is nothing for the extra probe to cover. Test now asserts Get-CimInstance is absent and that the remaining probes appear in the agent's order, so neither a new probe nor a reorder passes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
|
Valid, fixed in ac0028a — and this time by deleting code rather than adding it. I'd put
{ (Get-WmiObject Win32_BIOS -ErrorAction Stop).SerialNumber },
{ (& wmic bios get serialnumber /format:list) ... },
{ (& wmic bios get serialnumber) | Select-Object -Skip 1 }
-> $env:COMPUTERNAMEI checked the reason I'd reached for CIM in the first place —
So there was nothing for the extra probe to cover. The test now asserts Thanks for staying on this one — all four findings have been the same class of mistake, me reimplementing something the agent already does and diverging from it. That is also the root cause of the bug this PR exists to fix. |
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
🛡️ Automated Security Review (consensus)
3 findings — 0 high-confidence, 3 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.
🟡 Unsanitized download exception posted as scan_error.message
install.ps1:150 (also consumed at :238)
Impact: $script:LastDownloadError forwards raw .Exception.Message, which can embed request URIs (incl. query strings), proxy hosts, or local temp paths — widening exposure beyond the local console to anyone with report/DB access.
Fix: Sanitize before send (strip query strings, cap length) or post only exception type + HTTP status; keep full text in the existing Write-Warning.
Flagged by: Claude, Lead
🟡 Process-wide TLS protocol weakened via -bor assignment
install.ps1:85
Impact: OR-ing Tls12 into the existing SecurityProtocol leaves legacy protocols enabled on older .NET defaults, and on .NET 4.7+ can override SystemDefault and block TLS 1.3 — affecting this POST and the archive download in the same process.
Fix: Assign explicitly (Tls12 | Tls13 with try/catch for missing Tls13), or drop the line and rely on SystemDefault like the branch lookup already does.
Flagged by: Claude, Lead
🟡 Hostname fallback contradicts stated “no PII” boundary
install.ps1:59
Impact: When BIOS probes fail, device_id is $env:COMPUTERNAME, which may encode usernames/employee IDs — inconsistent with the PR’s “device serial only, not hostnames” claim (matches agent behavior, but changes the privacy posture of failure reports).
Fix: Update the PR/docs to acknowledge hostname fallback, or hash the fallback in both installer and WindowsDeviceIdExtractor together.
Flagged by: Claude
Previously acknowledged (not re-flagged)
- Cleartext HTTP Bearer key on failure report — fixed in
61ba347; HTTPS guard now mirrors branch lookup (@anonpran). - Placeholder BIOS serial accepted as
device_id— fixed in61ba347;INVALID_SERIALSnow mirrorsconstants.py(@anonpran). - Device-identity probe mismatch (CIM/extra probes, wrong order) — fixed in
f0adb82/ac0028a;Get-DeviceIdnow matches agent chain verbatim (@anonpran). - Empty outer
catchhides reporting failures — accepted by design; must not alter exit code or delay on an already-broken install (PR description: “Fails silently by design”).
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head ac0028a1 · 2026-09-02T05:11Z
The probes returned arrays and the caller stringified them, so PowerShell joined every line with a space. wmic emits a header, blank lines, and one row per BIOS instance, so a machine with more than one usable line would have reported "ABC123 DEF456" as its serial while the agent reported "ABC123" — a serial belonging to no machine, on a row the later scan never touches. The agent strips each line, drops the empties, skips the header and returns the first valid one. The loop now does the same, which also removes the need for each probe to normalise its own output. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
|
Valid, fixed in edb8888. The probes return arrays and the caller did Worse than a mismatch — that is a serial belonging to no machine at all, on a row no scan will ever touch again. The agent's shape is: strip each line, drop empties, skip the header, return the first valid one. foreach ($line in $lines) {
$serial = "$line".Trim()
if ($serial -and $INVALID_SERIALS -notcontains $serial.ToUpper()) { return $serial }
}Moving the line-splitting into the loop also means each probe no longer has to normalise its own output, so the three probes stay literal translations of the agent's three. |
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
🛡️ Automated Security Review (consensus)
0 findings — 0 high-confidence, 0 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.
No open security issues. Send-InstallerFailure mirrors the branch-lookup HTTPS guard, keeps the Bearer token out of the JSON body, and Get-DeviceId now follows the agent's probe chain, placeholder-serial rejection, and first-valid-line parsing. Semgrep and Gitleaks are clean.
Previously acknowledged (not re-flagged)
- HTTP report could send API key over cleartext (
install.ps1) — Fixed in 61ba347; maintainer added the same^https://guard used by the branch lookup. - Placeholder BIOS serial submitted as
device_id(install.ps1) — Fixed in 61ba347; maintainer alignedINVALID_SERIALSand hostname fallback withconstants.py/ the agent. - Device-ID probe chain diverged from the agent (CIM extra probe, missing WMI/WMIC steps) (
install.ps1) — Fixed in f0adb82 and ac0028a; maintainer replaced the custom chain with the agent's four-step sequence verbatim. - WMIC multi-line output joined into a bogus serial (
install.ps1) — Fixed in edb8888; maintainer now takes the first valid line per probe, matching the agent. - Empty
catchblocks make reporting failures undiagnosable (install.ps1) — Accepted by design; PR author: reporting must fail silently on an already-broken install (no exit-code change, no added delay).
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head edb88883 · 2026-09-02T05:30Z
device_id is a primary key on the scan lifecycle endpoint, so the installer had to resolve device identity exactly as the agent does or it would write a second device_discovery row for the same machine. That single requirement produced Get-DeviceId, a PowerShell copy of INVALID_SERIAL_VALUES, and five review findings, all of them subtle and all in the same function. As a Sentry tag, device_id cannot corrupt anything, so none of that is needed. Get-DeviceId and the constant copy are gone and constants.py is a single definition again. The serial is now a best-effort tag; a placeholder value is information rather than a collision. The event lands beside DISCOVERY-TOOL-SCRIPT-17 in the project where these failures are already investigated, tagged phase=installer_blocked with the reason, domain, branch and PowerShell version. The title carries the reason code only, so the issue does not fragment per machine the way the dispatch.lock errors did. Traded away: joining blocked machines to their org's device rows in SQL. The domain tag identifies the customer and device_id the machine, which answers the same question without a key that has to be exactly right. AI_DISCOVERY_SENTRY_DSN overrides the DSN and an empty value disables sending, matching how the agent is silenced in tests. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
|
Reworked the target after review. Failures now go to Sentry, not the scan lifecycle endpoint. The five findings on this PR were all in As a Sentry tag, The event lands in the same project as The title carries the reason code only, never the per-machine detail — that is the Traded away: joining blocked machines to their org's device rows in SQL. The Mutation-tested both new guards: |
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
🛡️ Automated Security Review (consensus)
3 findings — 0 high-confidence, 3 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.
🟡 Unredacted download exception forwarded to Sentry
install.ps1:140 → install.ps1:228 (payload at install.ps1:73)
Impact: repo-download-failed ships raw .Exception.Message in extra.detail; proxy URLs, query-string tokens, user:pass@ URLs, and local paths can reach Sentry and anyone with project access.
Fix: Classify failures (tls-error, http-403, etc.) and send a scrubbed, length-capped detail — strip ://…@, query strings, and path segments before upload.
Flagged by: Claude
🟡 AI_DISCOVERY_SENTRY_DSN parsed without validation
install.ps1:48-49, install.ps1:76
Impact: A process-local env override (MDM, scheduled task, setx) can redirect installer telemetry — hostname, domain, branch, serial — to an attacker-controlled host while still using the embedded key material.
Fix: Allow-list the override against a Sentry DSN regex (or ignore non-matching values); empty/unset should remain the only “disabled” path.
Flagged by: Claude
🟡 Hardcoded ingest DSN enables third-party event injection
install.ps1:43-44
Impact: The committed DSN is trivially scrapable from the distributed script; anyone can POST noise into the same project as agent telemetry (DISCOVERY-TOOL-SCRIPT-17), burning quota and diluting installer_blocked signal.
Fix: Acceptable if intentional — add Sentry inbound filters / spike protection, per-key rate limits, and/or a separate installer-only DSN so poisoning cannot mask agent events.
Flagged by: Claude
Previously acknowledged (not re-flagged)
- Silent
catchon reporting failures — deliberate: must not change exit code or delay on an already-broken install (“Fails silently by design”, PR description). - Hostname / BIOS serial / domain in Sentry tags — accepted: matches existing agent tagging practice (“No PII beyond existing practice”, PR description).
- Exact
device_idparity with the agent probe chain — accepted tradeoff after Sentry redesign:device_idis a best-effort tag, not a DB primary key; SQL row correlation was explicitly traded away (@anonpran). - HTTP Bearer key on explicit
http://Domain — fixed in61ba347(HTTPS guard mirrored from branch lookup). - Placeholder serial / CIM–WMI probe mismatches / WMIC array join — fixed, then superseded by removal of
Get-DeviceIdwhen reporting moved off the lifecycle endpoint.
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 60729991 · 2026-09-02T06:13Z
The tag claimed to be the agent's device_id while carrying an unvalidated BIOS value, so a placeholder or an empty string looked like a device identity rather than a hardware reading. Renaming it removes the claim: bios_serial is the raw value and is allowed to be junk or absent, which is itself a useful signal about the machine. Correlation was never resting on it. The agent tags hostname on every Sentry event (utils.py:2243, platform.node()) and this event tags $env:COMPUTERNAME, which is the same value on Windows, so installer and agent events for one machine already join on hostname whatever the BIOS returns. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
|
Half agree, and I've fixed the half I agree with — but by renaming the tag rather than reinstating the identity chain. Agreed: calling it Disagree that the chain is needed, for three reasons: 1. Correlation was never resting on the serial. The agent tags 2. A tag cannot misattribute. On the previous DB design 3. The lookup the serial would enable does not apply. Its unique value over hostname is finding the machine in our dashboard — but a blocked machine has no device row at all, because the agent never ran. That is the entire premise of this PR. And once it later scans successfully, the correlation works out anyway: if the agent keyed that row on the serial then the serial was valid and mine matches; if it fell back to hostname, the hostname tag matches. So the chain would add ~25 lines and a second copy of Happy to be overruled if you see a query that needs installer events keyed on the agent's resolved |
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
🛡️ Automated Security Review (consensus)
2 findings — 0 high-confidence, 2 to triage. Reviewers: Claude, Semgrep, Gitleaks, Lead (Cursor unavailable).
🟡 Unsanitized download exception text posted to Sentry
install.ps1:141, install.ps1:72
Impact: repo-download-failed events ship the raw .Exception.Message, which can embed proxy credentials (http://user:pass@...), tokenized URLs, or C:\Users\<name>\... paths into a third-party store visible to all Sentry project members.
Fix: Sanitize extra.detail before send (strip URL userinfo/query, redact user-profile paths, cap length) or send only exception type plus HTTP status.
Flagged by: Claude, Lead
🟡 Hardcoded public Sentry DSN in distributed installer
install.ps1:42
Impact: The ingest key is client-visible by Sentry design, but anyone with the repo can forge installer_blocked events or burn project quota, degrading the signal this PR adds.
Fix: Enable per-key rate limits/spike protection; consider a dedicated installer DSN/key so abuse can be rotated without touching agent reporting.
Flagged by: Claude
Previously acknowledged (not re-flagged)
- Empty outer
catch {}onSend-InstallerFailure— Maintainer: fails silently by design; must not change exit code or add delay on an already-broken install. AI_DISCOVERY_SENTRY_DSNfully overrides the DSN — Maintainer: intentional; empty disables sending and matches how the agent is silenced in tests.bios_serial/hostnametags without agent identity chain — Maintainer: accepted design after Sentry rework; tags are not DB keys,hostnamecorrelates with agent events, and blocked machines have no device row to join anyway.- Greptile HTTP Bearer-key /
device_idprobe findings — Addressed in prior commits (HTTPS guard, probe parity, then Sentry migration); not re-raised.
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 8335ea34 · 2026-09-02T06:46Z
The old wording referred to "these paths" from the function definition, pointing at call sites 170 lines below. The question a reader actually has is why this does not use utils.py's Sentry reporter, and the answer is that the agent is not downloaded yet at that point. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
Bugbot needs on-demand usage enabledBugbot uses usage-based billing for this team and requires on-demand usage to be enabled. A team admin can enable on-demand usage in the Cursor dashboard. |
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
🛡️ Automated Security Review (consensus)
1 finding — 0 high-confidence, 1 to triage. Reviewers: Cursor (unavailable), Claude, Semgrep, Gitleaks.
🟡 TRIAGE — Download exception text may leak credentials to Sentry
install.ps1:141 → install.ps1:74
Impact: $script:LastDownloadError forwards the raw .Exception.Message into extra.detail; PowerShell web/proxy errors often embed full request URIs or echoed auth headers, which would be stored in Sentry off-box.
Fix: Scrub URLs/query strings and cap length before send (e.g. exception type + HTTP status); only include a redacted message fragment.
Flagged by: Claude
Previously acknowledged (not re-flagged)
- HTTP Bearer key over cleartext
http://domain — Fixed in61ba347; Sentry path no longer posts to the scan API, and the prior HTTPS guard issue was addressed. - Device identity probe chain / placeholder serials /
device_idtag — Resolved by removingGet-DeviceIdand switching to Sentry;bios_serialis an intentional best-effort hardware tag, withhostnameas the correlation key (@anonpran). - Empty outer
catchonSend-InstallerFailure— Accepted by design: must not change exit code or add delay on an already-broken install (PR description). hostname/bios_serialin Sentry tags — Accepted: matches existing agent telemetry practice (“no PII beyond existing practice”).AI_DISCOVERY_SENTRY_DSNenv override — Accepted: matches how the agent is silenced in tests; URI construction forceshttps://for the ingest host.- Hardcoded Sentry DSN — Accepted: client-side ingest keys are public by design and match the agent’s existing pattern; enable Sentry inbound filters/rate limits if quota abuse is a concern.
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head bfbb8cc2 · 2026-09-02T07:01Z
install.ps1:181 exits when -ApiKey or -Domain is absent, which is the same silent-exit shape the other two gates had. An MDM push with a malformed argument dies earliest and told nobody. Self-selecting by construction: Send-InstallerFailure returns early without a resolved domain, so this fires for a missing key and stays quiet for a missing domain, which has nothing to attribute the event to anyway. Detail carries booleans, never the key itself. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
🛡️ Automated Security Review (consensus)
4 findings — 0 high-confidence, 4 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.
🟡 Unscrubbed exception text sent to Sentry
install.ps1:141 → install.ps1:76
Impact: repo-download-failed forwards $_.Exception.Message verbatim in extra.detail; .NET web errors can embed full URLs, proxy/auth endpoints, or local paths (usernames) into a shared third-party project.
Fix: Scrub/truncate before send (strip user:pass@, token-like substrings) or map to a small set of classified reason strings; keep raw text local-only.
Flagged by: Claude, Lead
🟡 AI_DISCOVERY_SENTRY_DSN can redirect telemetry
install.ps1:43
Impact: Any process that can set the installer's environment can point events (hostname, bios_serial, domain, exception detail) at an arbitrary HTTPS host; the empty outer catch hides failed/malicious delivery.
Fix: Only honor overrides whose host matches an allowlist (e.g. *.ingest.sentry.io / *.sentry.io); treat empty as disabled; fall back to the built-in DSN otherwise.
Flagged by: Claude, Lead
🟡 TLS 1.2 OR'd without disabling legacy protocols
install.ps1:74
Impact: [Net.ServicePointManager]::SecurityProtocol -bor Tls12 leaves SSL 3.0 / TLS 1.0 enabled for the rest of the PowerShell session, slightly widening downgrade surface for later HTTPS in the same process.
Fix: Assign explicitly, e.g. = Tls12 (and Tls13 where supported) instead of OR-ing into the existing bitmask.
Flagged by: Claude, Lead
🟡 Hardcoded public Sentry DSN enables event flooding
install.ps1:42
Impact: The write-only DSN is readable from the shipped installer; anyone can POST forged installer_blocked events and pollute or consume project quota, diluting the signal this PR adds.
Fix: Accept the tradeoff with inbound Sentry filters/rate limits on phase:installer_blocked, or rotate/limit at the project level.
Flagged by: Claude, Lead
Previously acknowledged (not re-flagged)
- Bearer key over cleartext HTTP — Fixed in
61ba347; reporting moved off/api/v1/ai-tools/report/to Sentry entirely. - Empty outer
catchon reporting — Fail-silent by design so a broken install is not delayed or given a different exit code. bios_serial/ hostname / domain in Sentry tags — Accepted as matching existing agent telemetry practice; relabelled fromdevice_idbecause tags are not DB primary keys and hostname is the correlation key.- Full device-ID probe chain in PowerShell — Removed after pivot to Sentry; maintainer accepted best-effort
bios_serialtag over duplicating agent identity logic.
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 0abe7a49 · 2026-09-03T05:01Z
…e separable
send_scan_event delegates to send_report_to_backend, so its failures were already
reaching Sentry -- but stamped "phase: send_report", identical to an inventory
upload failure. In the events we have today there is no way to tell whether a
device died announcing itself or died delivering results. Those are opposite
diagnoses: the first never got off the ground, the second scanned fine and could
not deliver.
Pass phase=scan_event:<event> from send_scan_event, and flip the six
report_to_sentry call sites from {**ctx, "phase": "send_report"} to
{"phase": "send_report", **ctx} so a caller-supplied phase wins instead of being
clobbered by the spread order. Default is unchanged for direct callers.
Makes "how many devices never started" answerable as a Sentry query.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
3606f61 to
d00244c
Compare
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
🛡️ Automated Security Review (consensus)
4 findings — 1 high-confidence, 3 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.
Repo-download exception text forwarded verbatim to Sentry
install.ps1:141, install.ps1:76, install.ps1:232
Impact: Raw .NET/PowerShell exception messages in extra.detail can include proxy hosts, local temp paths (often username-bearing), and download URLs—if those URLs ever carry tokens or pre-signed credentials, they leave the machine unredacted.
Fix: Scrub sensitive patterns (URLs, token/key/password query params) and cap detail length (e.g. 1024 chars) before Send-InstallerFailure.
Flagged by: Claude, Cursor
AI_DISCOVERY_SENTRY_DSN can redirect reports to an arbitrary host
install.ps1:42
Impact: Any principal that can set a user or machine environment variable can point installer telemetry (hostname, BIOS serial, tenant domain, branch, failure detail) at an attacker-controlled ingest endpoint—low-noise exfiltration without elevation.
Fix: Only honor overrides that match an expected Sentry DSN shape/host allow-list; treat anything else as disabled.
Flagged by: Claude
Embedded Sentry DSN enables forged installer_blocked events
install.ps1:41
Impact: The public client key is write-only by design, but anyone with the script can inject unlimited forged events into the same project, polluting the signal this PR adds and consuming quota.
Fix: Enable per-key rate limiting and inbound filters in Sentry; accept as trade-off only if it mirrors the agent's existing embedded DSN policy.
Flagged by: Claude
TLS protocol bitmask OR leaves legacy protocols enabled
install.ps1:72
Impact: -bor Tls12 adds TLS 1.2 without disabling older defaults (Ssl3, Tls 1.0) for the remainder of the process; a network attacker could theoretically downgrade, though Sentry ingest itself requires TLS 1.2+.
Fix: Assign the allowed set explicitly (Tls12 / Tls13 inside the existing try) instead of OR-ing onto the process default.
Flagged by: Claude
Previously acknowledged (not re-flagged)
- Device identity probe mismatches / placeholder serial as
device_id— Maintainer removedGet-DeviceIdand the duplicatedINVALID_SERIAL_VALUESchain; failures now go to Sentry as tags.bios_serialis an honest raw hardware reading (not resolveddevice_id); correlation useshostname, and tags cannot corrupt DB rows. - HTTP Bearer key sent over cleartext
http://domain — Superseded: reporting now posts directly to Sentry over HTTPS (https://$sentryHost/.../store/), not the tenant/api/v1/ai-tools/report/endpoint. - Empty outer
catchonSend-InstallerFailure— Accepted by design: must not change exit code or add delay on an already-broken install (10s cap, fail silently). - Hostname / BIOS serial in telemetry — Accepted: matches existing agent practice (“No PII beyond existing practice” in PR description).
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head d00244c7 · 2026-09-03T05:13Z
Under MDM there is no console attached, so every message install.ps1 prints is discarded when the process exits. A customer whose rollout failed has nothing to send us, and Sentry only helps if their network lets us reach it -- Xome has 143 devices and zero reports in 14 hours. Reuses setup-scheduled-scan.ps1's convention rather than inventing one: %LOCALAPPDATA%\Unbound\Logs, same timestamped line format, install.log next to scheduled.log. One directory for support to ask about, not two. Write-Log never raises, so logging cannot become the failure. Write-ErrorMessage now echoes the resolved path, so the reply to a customer is a copy-paste rather than an explanation of the convention. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
🛡️ Automated Security Review (consensus)
2 findings — 0 high-confidence, 2 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.
🟡 Unbounded exception detail sent to Sentry and written to disk
install.ps1:158, install.ps1:251 · Claude
Download failures stash the full Exception.Message in $script:LastDownloadError and forward it unchanged to Sentry (extra.detail) and install.log via Write-Warning/Write-ErrorMessage, with no length cap or scrubbing.
Impact: Proxy/HTTP/TLS errors can embed request URIs or environment-specific paths; if any download URL ever includes the API key, that value leaves the machine to a third party and is persisted locally.
Fix: Mirror the Python side (response_body[:1024], curl_stderr[:1024]): redact $_key/$ApiKey from $Detail before Send-InstallerFailure and Write-Log, then truncate to 1024 characters.
🟡 Multi-line exception text can forge log lines
install.ps1:38 · Claude
Write-Log formats "[{timestamp}] {msg}" with $msg verbatim; exception strings containing CR/LF produce additional lines that look like independent timestamped entries.
Impact: install.log is the support artifact customers are asked to send; injected line breaks weaken it as trustworthy evidence.
Fix: Flatten before writing: $msg = ($msg -replace '[\r\n]+', ' ').
Previously acknowledged (not re-flagged)
- HTTP Bearer key over cleartext
http://domain — Fixed in 61ba347; reporting later moved off the backend endpoint to Sentry entirely (@anonpran). - Device identity mismatch (
device_id, placeholder serials, CIM/WMIC probe chain) — Reworked to best-effortbios_serialSentry tag; correlation relies onhostname, which the agent already tags (@anonpran: tag cannot misattribute; blocked machines have no device row). - Silent
catch { }onSend-InstallerFailure— Accepted by design: must not change exit code or add delay on an already-broken install (PR design notes). AI_DISCOVERY_SENTRY_DSNenvironment override — Deliberate; mirrors agent test silencing (PR design notes).hostname/bios_serialsent to Sentry — Accepted; no PII beyond existing agent practice (PR design notes).
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 3b34cb0e · 2026-09-03T05:21Z
Both review findings share a cause: Exception.Message is third-party, unbounded and forwarded verbatim. One Format-Detail helper on both sinks. Flatten CR/LF: Write-Log formats "[ts] $msg", so an embedded newline produces extra lines that read as separate entries. install.log is the artifact we ask customers to send, so it has to stay one-entry-per-line. Redact the key: no current download URL carries it (the archive comes from github.com and the branch check sends a Bearer header), but the log is written to disk and the Sentry payload leaves the machine, so this is cheap insurance rather than a fix for a known leak. Cap at 1024, matching response_body[:1024] and curl_stderr[:1024] on the Python side. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
vigneshsubbiah16
left a comment
There was a problem hiding this comment.
🛡️ Automated Security Review (consensus)
3 findings — 0 high-confidence, 3 to triage. Reviewers: Cursor, Claude, Semgrep, Gitleaks.
🟡 TRIAGE
1. AI_DISCOVERY_SENTRY_DSN can redirect installer telemetry to an arbitrary host
install.ps1:75
- Impact: A process that can set
$env:AI_DISCOVERY_SENTRY_DSN(e.g. per-userHKCU\Environment) can point failure reports at a server they control, exfiltrating hostname,bios_serial, customerdomain, branch, and exception detail; the fire-and-forget call hides the misdirection. - Fix: Honor the override only when the parsed host matches an allowlist (e.g.
*.ingest.*.sentry.io), or restrict overrides to an explicit test-only switch and keep production on the embedded DSN. - Flagged by: Claude
2. Exception detail redaction is literal-only; credentials in third-party text may still leak
install.ps1:36
- Impact:
Format-Detailscrubs only an exact match of$_key; proxy URLs (user:pass@), URL-encoded keys, and other embedded secrets in download/proxy errors can reach Sentryextra.detailand%LOCALAPPDATA%\Unbound\Logs\install.log(a support artifact). - Fix: Add pattern-based scrubbing (URI credentials, case-insensitive and URL-encoded key forms) on top of the literal replace; consider reusing the Python-side redaction rules if they exist.
- Flagged by: Claude
3. TLS protocol set uses -bor, leaving weaker protocols enabled
install.ps1:98
- Impact:
[Net.ServicePointManager]::SecurityProtocol -bor Tls12adds TLS 1.2 without disabling SSL 3.0 / TLS 1.0 already enabled in the process, mutating TLS behavior for all subsequent HTTPS in the installer (archive download included). - Fix: Assign explicitly, e.g.
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 -bor [Net.SecurityProtocolType]::Tls13. - Flagged by: Claude
Previously acknowledged (not re-flagged)
- HTTP
Domainsent Bearer API key over cleartext — Fixed in 61ba347; reporting later moved off the backend endpoint entirely (@anonpran). - Placeholder / mismatched
device_idand multi-probe identity chain — Fixed across several commits, then removed with the Sentry rework;device_idis nowbios_serialas an honest best-effort tag, not a primary key (@anonpran). domain,hostname, and rawbios_serialin Sentry events — Accepted as within existing agent telemetry practice;hostnameis the correlation key (@anonpran).- Empty
catchonSend-InstallerFailurehides reporting failures — Accepted by design: prerequisite exits must not change exit code or add delay beyond the 10s cap (PR description).
🤖 consensus review · reviewers: Cursor,Claude,Semgrep,Gitleaks · head 8f66bc1e · 2026-09-03T05:42Z
Make the Windows installer report why it gave up, instead of exiting silently.
The gap
The agent is what reports. Every exit above it is invisible:
Git was a hard Windows prerequisite from 4 Feb until #262. It printed
Git is not installed.to a console nobody reads. The only visible symptom was 69 Xome devices that checked in via MDM and had no discovery row, with nothing to explain them. Python is the remaining gate of the same shape.The change
Send-InstallerFailureposts a Sentry event before each of those exits:It lands in the same project as
DISCOVERY-TOOL-SCRIPT-17, so blocked installers sit next to zero-tool scans where these get investigated.Reason codes are
no-pythonandrepo-download-failed. The latter carries the real exception text, so a certificate failure, a proxy 403 and a missingExpand-Archiveare three different answers rather than one shrug.Why it can report at all
install.ps1already makes an authenticated HTTPS call a few lines earlier, for the branch lookup. Network and TLS are proven on that machine before either gate. The capability was always there; it was never used.Design notes
Sentry, not the scan lifecycle endpoint. The first version of this PR posted
scan_event=failedto/api/v1/ai-tools/report/. That madedevice_ida primary key, which meant the installer had to resolve device identity exactly as the agent does or write a duplicatedevice_discoveryrow. That single requirement produced ~30 lines of probe logic, a PowerShell copy ofINVALID_SERIAL_VALUES, and five review findings. As a tag,device_idcannot corrupt anything, so all of it went away andconstants.pyis a single definition again.Low-cardinality title. The reason code is in the title, the varying detail is in
extra. Thediscovery.dispatch.lockerrors put a username in the message and split one bug into 44 issues, which is a large part of why it went unnoticed for months.Fails silently by design. Wrapped, 10s cap,
$null =on the call. This runs on an already-broken install and must not change the exit code or add delay.No PII beyond existing practice. Reason codes, versions, hostname and serial. The agent already tags
hostnameon every event.AI_DISCOVERY_SENTRY_DSNoverrides the DSN; empty disables sending. Matches how the agent is silenced in tests.Testing
Cross-platform cases in the class that already owns this file. The gate-coverage test walks back from each exit's error text and asserts a report precedes it, so a third gate added later without reporting fails the build.
Every guard mutation-tested:
No
pwshon my machine, so CI onwindows-latestis the first real execution of the PowerShell — worth a reviewer's eye on syntax.Not in scope
install.shhas the same Python gate. Worth saying plainly: the Git bug existed because the two installers drifted after the Feb archive-fallback change touched onlyinstall.sh. Doing Windows alone repeats that, so this should not sit long.The warnings are untouched.
"No active user logged in. Discovery may return 0 results."predicts an empty scan in plain English and we never hear it either. That belongs on the scan the agent is about to start, so it needs a different mechanism.🤖 Generated with Claude Code
https://claude.ai/code/session_01A7m2NkfcYw536ZbmPJ7kfu
Greptile Summary
The PR adds persistent Windows installer diagnostics and reports prerequisite failures to Sentry, while adding scan-event phase tags to distinguish backend delivery failures.
Confidence Score: 5/5
The PR appears safe to merge because no blocking failure remains.
No blocking failure remains.
Important Files Changed
Reviews (13): Last reviewed commit: "Scrub third-party exception text before ..." | Re-trigger Greptile
Context used: