Security

Markdown

Privacy

What leaves the machine, for each feature. This page follows docs/SPEC.md section 7; the threat model and how each rule is enforced are in docs/SECURITY-MODEL.md.

There is no telemetry, no analytics and no phone-home to Qiick or any third party. Besides the public data sources of the checks, the only destination is your own excubia-app, and only for the features you turn on.

FeatureDefaultSends toWhat
ChecksonPublic data sourcesPackage names and versions, runtime versions
Pushoff until URL and token are setYour excubia-appThe JSON report
Runtime monitoringoff (runtime.enabled)Your excubia-appAggregates per minute
Exception captureoff (exceptions.enabled)Your excubia-appMasked exception groups
Health endpointoff (healthEndpoint.enabled)Whoever has the secretCheck statuses

Checks

excubia check and the Artisan commands send only package names and versions, and runtime versions, to the public APIs of the checks: endoflife.date, Packagist (metadata and security advisories), OSV.dev and the GitHub API. No file contents, .env values, source code, paths or hostnames.

  • Private packages are never sent: see below.
  • GITHUB_TOKEN is optional, read from the environment only, sent only to the GitHub API as a bearer header, and never printed or written to the cache or the report.
  • All HTTP is over TLS with timeouts (connect 5 s, total 15 s) and limited retries; HTTPS_PROXY is respected.
  • Excubia never writes to the scanned project except the --output file, and vendor/bin/excubia never executes project code.
  • The Artisan commands run inside the booted app. The posture checks read it without writing, except the scheduler heartbeat cache key excubia:scheduler-heartbeat, and their findings carry only booleans, counts, ages and names: never .env values, DSNs, hostnames, paths or exception messages.
  • The response cache stores public API responses only, in the system temporary directory unless cache.directory says otherwise.

Private packages

Only packages the lockfile shows as served by the public registries are sent (#108):

LockfileSentNot sent
composer.locknotification-url is https://packagist.org/downloads/path or artifact dist, VCS repositories, Satis, Private Packagist (another notification-url), no notification-url
package-lock.jsonresolved starts with https://registry.npmjs.org/link: true, file: and VCS dependencies, workspace members (no resolved), any other registry

The lockfile cannot tell a private package on the public npm registry, or one fetched through Packagist, from a public one. Name those in privacy.skipPackages with globs such as "acme/*" or "@acme/*" (configuration):

{ "privacy": { "skipPackages": ["acme/*", "@acme/*"] } }

Each check that would send names (composer-advisories, npm-advisories, composer-abandoned, composer-stale, laravel-next-blocker) reports the skipped packages in one skipped row with subject private packages, skipReason not-applicable and a message such as 2 private packages not sent to Packagist: acme/billing, acme/ui. The names come from your own lockfile and stay in the local report; with the opt-in push they reach only your own excubia-app, and the health endpoint shows the subject without the message. A check with no other finding then reports skipped; it never fails and --strict ignores it. Private packages are not checked for advisories, abandonment or staleness: watch them yourself.

Push

excubia push / excubia:push send the JSON report (docs/report-schema.json) to POST {url}/api/reports: package names and versions, runtime versions and findings (check id, severity, status, subject, message, fix hint, evidence URL, advisory id), inside an envelope with protocolVersion, batchId and sentAt.

  • Nothing is sent until an app URL and a project token are configured.
  • HTTPS only; plain HTTP only to localhost, 127.0.0.1 and [::1].
  • The token is sent as Authorization: Bearer and is never printed, cached or written to a report.

Runtime monitoring

Off until runtime.enabled is true. Sends only these aggregates per UTC minute, each with count, errorCount, totalMs, maxMs and its sampleRate (docs/runtime-schema.json):

  • requests: route name (or unnamed), HTTP method, status class (2xx, …);
  • slow queries: SQL text without bindings, with inline literals (single-quoted strings, numbers, hex and dollar-quoted literals) replaced by ?; double-quoted and backtick-quoted identifiers stay, and the connection name;
  • jobs: job class, queue and connection names, processed or failed;
  • scheduled tasks: the description you set (paths relative to the project, secrets such as token=… masked as in exception messages), else artisan <command name> for Artisan commands or the executable's file name for exec() tasks, never their arguments, else Closure; finished, failed or skipped;
  • outgoing HTTP (Laravel's HTTP client): host and status class.

Never sent, and never written to the local buffer: URLs and query strings, request or response bodies, headers, cookies, users, IP addresses, query bindings, exception messages, .env values or absolute paths.

Exception capture

Off until exceptions.enabled is true, independent of runtime monitoring. One reportable() callback sees only what the app reports (dontReport applies), returns nothing so the app's own reporting is unchanged, and skips the classes in exceptions.ignore. Per group (same fingerprint, context and environment) it sends exactly (docs/exception-schema.json):

  • class (anonymous classes as Parent@anonymous), fingerprint, and the masked message (at most 1000 characters);
  • frames, innermost first, at most 50: file relative to the project root (vendor/<package>/… for dependencies, the bare file name outside the project), line, function (Class::method, closures without their line), inApp. Never arguments or local variables;
  • previous: the classes of chained exceptions (at most 5), without their messages;
  • context: request and the route name (unnamed without one), job and the job class, command and the Artisan command name without arguments, or unknown;
  • environment (app.env), count, firstSeen and lastSeen.

Masking, applied before anything is written to the local buffer, in this order:

WhatBecomes
Matches of exceptions.maskPatterns[masked]
Absolute paths (/…, C:\…, \\server\share\…)project root removed; other paths reduced to the file name
URL credentialsremoved
URL hosts and ports[host] (the scheme and path stay)
URL query strings and fragments?[query]
Quoted SQL literals, when the message carries SQL?
Database Host, Socket, Port, Database details[masked]
JWTs[jwt]
Bearer / Basic credentials[token]
Any other Authorization / Proxy-Authorization value (Token, Digest, AWS4-HMAC-SHA256…)[masked]
Host names after host:, hostname=, resolve host or getaddrinfo for[host]
password=, secret:, token=, api_key= and similar values[masked]
Emails[email]
IPv4 and IPv6 addresses[ip]
UUIDs[uuid]
Hex strings of 32+ characters, 20+ character strings mixing letters and digits (API keys)[token]
Phone-like numbers (+ and 8 or more digits with separators), US social security numbers, card-like runs of 13–19 digits, any number longer than four digits[number]

Query exceptions are rebuilt from the driver message, the connection name and the SQL without bindings. Never sent: request payloads, headers, cookies, users, IP addresses, query bindings, .env values or absolute paths.

Health endpoint

Off until healthEndpoint.enabled is true and EXCUBIA_HEALTH_SECRET is set to at least 32 characters. It sends nothing by itself; it answers a GET that carries the secret, with check ids, check names, statuses, severities, counts and package or runtime names. Finding messages, fix hints and evidence URLs are not included, so no path, hostname, token or .env value reaches the response. A wrong or missing secret gets 404, and the route is throttled (60 requests a minute per client IP).

Local files

PathWritten byContent
--output filecheckThe report you asked for
System temp qiick-excubia-<uid>/ or cache.directoryall checksPublic data-source responses
storage/excubia/runtime/runtime monitoringAggregates waiting to be sent, capped by runtime.bufferMaxBytes
storage/excubia/exceptions/exception captureMasked groups waiting to be sent, capped by exceptions.bufferMaxBytes
storage/excubia/health.jsonexcubia:check, excubia:push with the endpoint onThe last health result
Cache key excubia:scheduler-heartbeatexcubia:heartbeatA Unix timestamp

Report a privacy or security problem as described in SECURITY.md.


Source: docs/guides/privacy.md in excubia-core.