Enterprise security review: remove third-party asset loads, close egress, add trust artifacts - #4972
Conversation
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
Visual diffComparing 11 of 66 comparison(s) flagged (>1% pixel diff).
Folder: c96ad88f99e0. Full PNGs also attached as a workflow artefact. Generated by visual-diff bot. Pixel diffs >1% flagged; eyeball the table before merging. This check is non-blocking — fail = bot bug, not a code problem. |
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
|
Bugbot is not enabled for your account, so this pull request was not reviewed. Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs. |
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
|
Blocker: phantom CI cascade from main regression (not a real test failure) All 30 check runs on this PR complete in 2-6 seconds — including "Python dependency audit", "Vendored fonts not stale", "No third-party asset loads", and all Sync matrix jobs, which normally take 30-120 seconds each. These are phantom failures (runner never assigned, concurrency-cancelled). Root cause: main CI went red after PR #5008 merged (~2026-08-19T23:53 UTC). E2E Gate (required) is also phantom (3s, no runner). To unblock:
This PR (drift-bot ✅, no merge conflicts) is ready once main CI recovers. Generated by Claude Code |
Every dashboard page load contacted two external origins. That is three
separate problems, and it made "your data never leaves your network" false
for the self-hosted SKU before any daemon opened a socket:
fonts.googleapis.com (x4 head blocks)
Discloses every viewer's IP and User-Agent to a third-party processor on
every page load, and renders in a fallback face on an air-gapped install.
cdn.jsdelivr.net (chart.js + date adapter, x2)
Arbitrary third-party JavaScript with DOM access, executing in the page
that renders agent transcripts. A CDN compromise is a ClawMetry
compromise. No SRI was present, and none was possible: jsDelivr's own
minifier endpoint documents that SRI cannot be used with its dynamically
generated files. Both tags sat two lines under a comment reading
"Vendored + pinned (no external CDN, no supply-chain risk)".
Now vendored:
- 15 woff2 subsets for the dashboard, 13 for the v2 SPA, generated by
scripts/vendor_fonts.py. Deduped by content hash (60 files -> 15; Google
serves one variable file per subset) and unicode-range is preserved
verbatim, so a Latin-only viewer still fetches ~25 KB, not the whole set.
- chart.js 4.4.7 + chartjs-adapter-date-fns 3.0.0 from the npm registry
tarball, not the CDN's re-served copy.
Provenance, not just presence. scripts/vendor.lock.json records package,
version, tarball path, license and sha256 for all four vendored bundles;
scripts/verify_vendor.py re-downloads the registry tarball and byte-compares.
All four are byte-identical to their published releases.
While pinning, DOMPurify was found at 3.1.7, which OSV flags for
CVE-2025-26791 (mXSS via template literals, fixed in 3.2.4). Not reachable
here (that path needs SAFE_FOR_TEMPLATES:true, which we never set) but a
scanner flags the version regardless, so it is upgraded to 3.4.13. marked
stays at 13.0.3, which is advisory-clean; npm latest is 18.x and a 5-major
jump belongs in its own change with transcript-rendering tests.
Also fixes a latent packaging bug found on the way. .gitignore had a
duplicate `dist/` rule after the v2 un-ignore, and .gitignore is
last-match-wins, so clawmetry/static/v2/dist/ was silently re-ignored.
Already-tracked files were unaffected, which is why nobody noticed, but
every NEW file in a rebuilt bundle was being dropped -- a CI build from a
clean checkout would have shipped an incomplete SPA. The v2 fonts are the
first new files to land there, so this had to be fixed to ship them.
The v2 font source of truth is frontend/public/ because vite.config.ts sets
emptyOutDir:true; writing into dist/ alone would be deleted by the next
build. The checked-in bundle is mirrored so it works without a rebuild, and
--check verifies both copies.
Verified: 28 woff2 + both stylesheets + all four vendor bundles are present
in a built wheel, so pip installs ship them.
New CI (.github/workflows/supply-chain.yml, also weekly on cron):
- verify_no_external_assets.py fails on any absolute http(s) reference in a
fetch-causing tag across 54 served files. Confirmed it catches a
reintroduced CDN script tag.
- verify_vendor.py hash-pins and byte-compares against npm (--offline for
air-gapped runners).
- vendor_fonts.py --check fails on a stale or hand-edited stylesheet.
- pip-audit + CycloneDX SBOM artifact, and OpenSSF Scorecard to SARIF.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nt it all "Your data never leaves your network" is the whole self-hosted pitch, and a CISO checks it with tcpdump during the pilot, not by reading the docs. Three calls could still fire from a deployment that had declared itself private. The gate was keyed wrong. Both existing suppressions checked is_custom_endpoint(), which is true only when CLAWMETRY_ENDPOINT is repointed. A self-hosted SERVER container does not set that -- it *is* the endpoint -- so SELF_HOSTED=true alone suppressed nothing in that process. New clawmetry.endpoints.egress_suppressed() ORs the three ways an operator declares a deployment private: repointed endpoint, SELF_HOSTED, or CLAWMETRY_OFFLINE. Fixed: - api.ipify.org was ungated everywhere. It feeds one cosmetic startup banner line, and in exchange discloses to a third party that this network runs ClawMetry. Now returns None when suppressed. Both duplicate definitions of get_public_ip() are gated; the first is dead (shadowed by the second) but gated anyway so reordering cannot reintroduce the call. - Install telemetry and anonymous funnel analytics now use egress_suppressed() instead of is_custom_endpoint(). - The unattended-update PyPI check is skipped for private deployments. On an air-gapped network it can only ever time out, and on a monitored one it is unexplained egress mid-review. Upgrades there belong to the operator's change process. Every gate fails CLOSED. The pre-existing telemetry code caught an ImportError and fell through to the managed-cloud URL, so a partial install meant a customer's self-hosted server phoned home. All three now suppress on error: the cost of a missing banner line is nothing, the cost of an unexplained packet in a customer's network is a failed review. Not changed, and worth saying plainly: the configured push to your own ingest endpoint is untouched. That traffic is the product. docs/EGRESS.md is the artifact a security review actually asks for -- every destination, what it sends, when, and how to disable it, with a per-mode summary table and three commands the reviewer can run to verify it. Written from the code, and two claims in the first draft were wrong until the code was fixed to match (the PyPI check above, and the ipify call). Verified while writing it: no analytics or error-reporting SDK anywhere in the shipped tree -- no GA, Segment, Sentry, PostHog, Mixpanel, Amplitude, Hotjar, FullStory. The one upstream dependency that ships telemetry on by default (DeepEval -> PostHog) is explicitly disabled before import. tests/test_egress_suppression.py: 38 cases across all three private modes, including that a default managed-cloud install is NOT suppressed (so the suppression tests prove something), truthy/falsy flag parsing, an explicit operator-chosen telemetry collector still winning, and each gate failing closed when the check itself raises. Note: tests/test_flow_runs_endpoint.py has 8 failures, confirmed pre-existing on origin/main and unrelated to this change. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e table SECURITY.md was 19 lines with no contact address, no response times, no scope, and no version policy. "Contact the maintainer directly" is not a disclosure process, and it is the first document a vendor security review opens. Rewritten to state: - Where to report (security@clawmetry.com + private GitHub advisory) and what happens next: 2 business days to acknowledge, 5 to assess, 30 to fix a confirmed critical/high. Framed as targets from a small team rather than a contractual SLA, because promising an SLA we cannot staff is the same failure as the overclaiming elsewhere. - 90-day coordinated disclosure, reporters credited by default. - Supported versions: latest only, no LTS branch, fixes ship forward rather than being backported. - Scope, including what we specifically want reported (anything crossing a trust boundary around transcripts, anything reaching the dashboard without a credential, anything that gets code onto a node via update/licensing) and what is out (runtime bugs, already-rooted host, unexploitable scanner output). The Known Limitations section is deliberate. It names the loopback-without- credential trust assumption, the bearer token being accepted from a query parameter, the absence of SSO/SAML/OIDC/SCIM, and the audit log having no identity to attribute to. A reviewer finds all four in the first hour; finding them already written down converts "they're hiding things" into "they know their system". The compliance table says Not started for SOC 2, ISO 27001/42001 and pen test rather than leaving it implied. RFC 9116 security.txt served at /.well-known/security.txt by the dashboard itself, not just the marketing site — an on-prem instance is exactly where a finder is least likely to know who to tell. Unauthenticated by design: a vulnerability reporter has no credential. Falls back to an inline contact rather than 500ing if the file is unreadable, and resolves through the clawmetry package so it works from a wheel, not just the repo. tests/test_security_txt.py (9 cases) checks required fields, that Contact is a real URI scheme, that the route serves 200 as text/plain, and that Expires is neither past (RFC 9116 says a stale file is invalid — this test is the renewal alarm) nor absurdly far out. Verified the file ships inside a built wheel; dotfile directories are easy to lose in packaging. BEFORE MERGING: create the security@clawmetry.com alias. A published contact that bounces is worse than none. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ge plan Releases authenticate to PyPI with a long-lived PYPI_API_TOKEN. A standing credential that can publish to the index is exactly what a supply-chain reviewer asks about, and it is also what blocks PEP 740 attestations, which are only available through Trusted Publishing. - actions/attest-build-provenance signs every built artifact against the workflow run and commit. Anyone can check it: `gh attestation verify dist/clawmetry-*.whl --repo vivekchand/clawmetry`. Free, and it does not require trusting us about what produced a wheel. - Trusted Publishing is wired but gated on a PYPI_TRUSTED_PUBLISHING repo variable, NOT switched on. It cannot work until the publisher is configured on PyPI, and this repo auto-publishes on merge, so flipping it outright would break releases. Default path is unchanged and merging this is safe. The three switch-over steps are in the workflow comment. Worth noting for positioning: PyPI provenance is a stronger claim than the self-issued Ed25519 attestation competitors sell as a paid product. It is free, standard, and issued by an index we do not control. README: OpenSSF Scorecard, security policy and egress badges. The Scorecard badge resolves only after the workflow runs on the default branch with publish_results, so expect it to be blank until this merges. docs/TRUST_BADGES.md is the shopping list, prices verified today: - Free and worth doing: Trusted Publishing, OpenSSF Best Practices (passing only -- gold requires two maintainers, so it is out of reach and the doc says so), CSA STAR Level 1 CAIQ, CodeQL/Dependabot/private reporting/branch protection, SPF+DKIM+DMARC+HSTS. - Paid, in budget: Cyber Essentials ~GBP 300+VAT, which matters mainly because it bundles cyber liability insurance for UK entities under GBP 20m turnover -- and an MSA typically demands GBP 2-5M cover, a hard gate no engineering clears. FLAGGED as contingent: it needs a UK-registered entity, and if we trade through a US LLC it does not apply. - Explicitly not worth it now: SOC 2, ISO 27001/42001, pen test, CSA Valid-AI-ted ($595, over budget, same registry entry as the free listing). Start the SOC 2 clock alongside a real pilot, not speculatively. The doc leads with "badges do not close deals" and ends with three prerequisites that block harder than any certification: security@ must exist before the two files publishing it merge; /security 404s; /enterprise still promises a DPA, pentest summaries and an IR plan that do not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nedx-py cyclonedx-bom removed --outfile in the latest release; redirect stdout instead, which works across all supported versions.
Confirmed with the founder: ClawMetry trades as Instalabs LLC, a Wyoming (US)
entity. That settles the open question in TRUST_BADGES.md and changes what is
worth buying.
RULED OUT -- Cyber Essentials (~GBP 300+VAT) was the previous top paid
recommendation. It certifies UK-registered organisations only, and its
genuinely valuable part -- bundled cyber liability insurance for UK entities
under GBP 20m turnover -- is UK-only as well. A US LLC gets neither.
ADDED, and better suited to a US entity:
- CISA Secure Software Development Attestation. Free, self-attested, gates US
federal sales and increasingly cascades to federal contractors. Worth doing
now rather than later because its four attestation areas map almost exactly
onto what this branch already built: secure build environment, trusted
source-code supply chain (verify_vendor.py byte-comparison), data provenance
for internal and third-party code (vendor.lock.json + SBOM), and automated
vulnerability checking (pip-audit + Scorecard). Filing is mostly
transcription. Table in the doc maps each area to the artefact.
- Cyber / Tech E&O insurance, priced for the US market rather than assumed
from the UK bundle: ~$1,500-4,000/yr for bundled cover under ~$2M revenue.
Above the $500 budget and flagged as such, but listed because an MSA
routinely demands $2-5M cover -- the single most likely thing to stall a
signed pilot at the paperwork stage. Get the quote before a deal, bind when
a contract needs it.
GDPR: the first draft of this said "appoint an Article 27 representative, it
is a legal requirement we are not meeting." That was too confident. Article 27
applies only to controllers NOT established in the Union, and while the entity
is Wyoming, the sole owner-operator is NL-resident -- and establishment turns
on stable arrangements and where activity is actually carried out, not on
where the company is registered. If the business counts as EU-established,
Article 27 does not apply at all, GDPR applies directly under Art. 3(1), and
there is a lead supervisory authority (Dutch AP), which is a better answer to
give an EU buyer than a contracted mailbox -- and free. Rewritten to put the
establishment question first, with both branches costed, because buying a
representative we do not need and skipping one we do are both real failure
modes. Flagged as a lawyer question, not an engineering one.
The thing that actually blocks a deal is having NO answer, so the prerequisite
now reads "settle it and publish the conclusion" rather than "buy this".
SECURITY.md compliance table gains the row honestly ("Being determined"),
names the publishing entity, and points out that self-hosted and air-gapped
deployments sidestep the question entirely because no personal data reaches us.
Also: security@clawmetry.com now exists, so that prerequisite is struck.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
8d2a90c to
f91a1b4
Compare
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
✅ Drift Bot (ClawMetry): no drift detectedDrift Bot analyzed the changed files against this project's blueprints and requirements and found no drift. |
Makes the self-hosted claim true at the edges, and produces the artifacts a vendor security review asks for.
Why
clawmetry/selfhosted.pyalready documents "a self-hosted deployment makes NO calls to clawmetry.com". That was true of the server — but not of everything around it. Four gaps made the claim false before any daemon opened a socket.What was wrong
Every dashboard page load contacted two external origins.
fonts.googleapis.com(4 head blocks) — discloses every viewer's IP and User-Agent to a third-party processor on each page load; renders in a fallback face on an air-gapped install.cdn.jsdelivr.net(chart.js + date adapter) — third-party JavaScript with DOM access, executing in the page that renders agent transcripts. No SRI, and none possible: jsDelivr's own minifier documents that SRI cannot be used with its dynamically generated files. Both tags sat two lines under a comment reading "Vendored + pinned (no external CDN, no supply-chain risk)".The suppression gate was keyed wrong. Install telemetry and funnel analytics checked
is_custom_endpoint(), true only whenCLAWMETRY_ENDPOINTis repointed. A self-hosted server container does not set that — it is the endpoint — soSELF_HOSTED=truealone suppressed nothing in that process.api.ipify.orgwas ungated everywhere, buying one cosmetic startup banner line in exchange for telling a third party that this network runs ClawMetry.Gates failed open. The telemetry gate caught
ImportErrorand fell through to the managed-cloud URL, so a partial install meant a customer's self-hosted server phoned home.What changed
Assets vendored. 15 woff2 subsets for the dashboard + 13 for the v2 SPA, generated by
scripts/vendor_fonts.py— deduped by content hash (60 files to 15, 1.3 MB to 363 KB) withunicode-rangepreserved, so a Latin-only viewer still fetches ~25 KB. chart.js 4.4.7 and its adapter pulled from the npm registry tarball, not the CDN's re-served copy.Provenance, not just presence.
scripts/vendor.lock.jsonrecords package, version, tarball path, license and SHA-256 for all four vendored bundles;scripts/verify_vendor.pyre-downloads the registry tarball and byte-compares. All four are byte-identical to their published releases.Egress closed. New
clawmetry.endpoints.egress_suppressed()ORs the three ways an operator declares a deployment private (repointed endpoint,SELF_HOSTED,CLAWMETRY_OFFLINE). Applied to telemetry, funnel analytics, the public-IP lookup, and the unattended-update PyPI check. Every gate now fails closed. The configured push to your own ingest endpoint is untouched — that traffic is the product.Trust artifacts.
docs/EGRESS.mdlists every destination, what it sends, when, and how to disable it, with three commands a reviewer can run.SECURITY.mdrewritten from 19 lines to a real policy with response targets, scope, supported versions, an honest compliance table, and a deliberate Known Limitations section. RFC 9116security.txtserved by the app itself, so an on-prem instance can answer "who do I report this to?"Supply chain in CI (
.github/workflows/supply-chain.yml, also weekly): external-asset scan, vendor byte-comparison, font staleness check,pip-audit, CycloneDX SBOM, OpenSSF Scorecard to SARIF.Release provenance.
actions/attest-build-provenancesigns every artifact. PyPI Trusted Publishing is wired but gated off behind a repo variable — it cannot work until the publisher is configured on PyPI, and this repo auto-publishes on merge.Found along the way
SAFE_FOR_TEMPLATES:true, which we never set — but scanners flag the version regardless. Upgraded to 3.4.13..gitignorebug. A duplicatedist/rule after the v2 un-ignore silently re-ignoredclawmetry/static/v2/dist/. Already-tracked files were unaffected, which is why it went unnoticed, but every new file in a rebuilt bundle was being dropped — a CI build from a clean checkout would have shipped an incomplete SPA. The v2 fonts are the first new files to land there.Testing
test_egress_suppression.py,test_security_txt.py), including that a default managed-cloud install is not suppressed, so the suppression tests prove something.security.txtship inside a built wheel.origin/main; both produce an identical set of 38 pre-existing failures.Before merging
security@clawmetry.com—SECURITY.mdandsecurity.txtboth publish it. A published contact that bounces is worse than none.clawmetry.com/securitycurrently 404s and/enterprisepromises a DPA, pentest summaries and an IR plan that do not exist. Both live in the landing-site repo.See
docs/TRUST_BADGES.mdfor the follow-on certification plan.🤖 Generated with Claude Code