Please do not open a public issue for a security problem.
Report it privately through GitHub: Report a vulnerability. That opens a draft advisory only you and the maintainers can see.
Include what you'd want if you were fixing it: the version (cifail --version), the
platform, what an attacker can do, and the smallest reproduction you have. If a log is
part of the reproduction, redact it first — see Logs are untrusted input.
Expect an acknowledgement within a week. Once a fix is ready it ships in a patch release and the advisory is published with credit, unless you'd rather not be named.
cifail is pre-1.0 and there is no long-term support branch: fixes go into the next
release from main. Only the latest release is supported.
| Version | Supported |
|---|---|
| 0.7.x | ✅ |
| 0.6.x | ❌ — superseded |
| 0.5.x | ❌ — superseded |
| 0.4.x | ❌ — superseded |
| 0.3.x | ❌ — superseded |
| 0.2.x | ❌ — superseded. Note 0.2.x runs rule-pack regexes with no timeout; see Rule packs execute nothing — but their regexes do run. |
| 0.1.0 | ❌ — superseded, and its release assets never resolved (see CHANGELOG.md) |
cifail is local-first and offline by default. It makes no network calls of its own unless you turn one of these on:
| Feature | Network | Default |
|---|---|---|
analyze, history, stats, clusters, rules, gate |
none | — |
--server / serve |
your own cifail server | off |
AI suggestions, embeddings, suggest-rule |
your Ollama host (localhost by default) | off |
| Notifications (Slack, webhook, Discord, Teams, SMTP, GitHub issues) | the endpoint you configure, server-side only | off |
There is no telemetry, no crash reporting, and no usage reporting — not opt-out, none at all. Nothing is sent anywhere you did not configure.
The one thing worth understanding: CI logs frequently contain secrets — tokens echoed by a misbehaving step, connection strings in a stack trace, signed URLs in a curl trace.
When cifail records a failure it persists an excerpt of the matched log region and a bag of terms from it. So:
~/.cifail/history.db(or your configured database) can contain whatever secrets were in the logs you analyzed. Treat it with the same care as the logs themselves.cifail analyze --no-historystill compares against history but does not write the current run to it.- Fingerprints and similarity run over a scrubbed form of the log (paths, numbers and GUIDs collapsed). Scrubbing is for matching, not for redaction — do not rely on it to remove secrets.
- Anything you attach to a public issue, including a log for a rule request, is public forever. Redact before you paste.
cifail config deliberately never prints a secret — it reports only whether one is
set, and which env var it came from. A test plants a password and a webhook URL and
asserts neither reaches stdout or stderr.
serve is the only component that listens on a network. It is meant for a trusted
network or behind an authenticating proxy, and:
- Started without a token it runs open — every route except the probes and the sign-in
flow is unauthenticated — and logs a loud warning. Always set
CIFAIL_SERVER_TOKEN(or--token) in anything that isn't a laptop. - Prefer per-client tokens (
CIFAIL_SERVER_TOKENS,--tokens-file) so one client can be revoked without rotating everyone's. All comparisons are constant-time. - Mutual TLS is available (
--client-ca <pem> --tls-cert <pfx>): clients whose certificate does not chain to your CA are rejected at the handshake. - The sign-in route is rate-limited (10 attempts per minute per client address). It has to be public — a browser signing in has no cookie yet — so without a limiter the server token could be guessed at network speed.
- The dashboard's session cookie is HttpOnly and
SameSite=Strict,Secureover HTTPS, and expires after 12 hours;POST /ui/logoutclears it. Note the cookie value is the bearer token, so treat it as one. POST /analyzecaps the log it will accept at 10 MB and answers413beyond that; the list endpoints clamp?limit=. Unbounded, either was a memory-exhaustion lever for anyone who could reach the port./healthz(liveness) never touches the store;/readyz(readiness) does. Both are public, because the kubelet has no token.- The API serves stored log excerpts, so anyone who can read the API can read those excerpts. See above.
Not yet addressed: the API is not rate-limited beyond the sign-in route, and notifications
are dispatched on the request thread, so a slow webhook slows the /analyze that triggered
it.
See deploy/README.md for the deployment wiring.
- Releases are built by
.github/workflows/release.ymlon GitHub-hosted runners from a tagged commit. Nothing is built or signed locally. - The tagged commit must be on
main, and nothing is published to nuget.org or GHCR until the maintainer approves the run's deployment to a protectedreleaseenvironment, which administrators cannot bypass, and that approval is only requested once the draft release has installed and run on Linux and macOS. Each job is granted only the permissions it uses: the jobs that build and test the binaries and the NuGet package never hold a token that can publish. The Docker image is the exception — it is built in the approved job that pushes it, inside BuildKit containers that do not see the registry login. - Version tags cannot be moved or deleted, and a published GitHub release is immutable, so a version number always refers to the same code and the same assets. A fix ships as a new version.
- Every action the workflows use is pinned to a full commit SHA, not a tag.
- Every binary archive ships a
.sha256, and each release an aggregateSHA256SUMS.scripts/install.shverifies the checksum and refuses to install without it (override only withCIFAIL_SKIP_CHECKSUM=1). - The NuGet package is published with trusted publishing (OIDC): no long-lived NuGet API key exists for this repository, so there is none to leak.
- Binaries are not code-signed or notarized yet. macOS Gatekeeper and Windows SmartScreen will warn on first run.
- The GitHub Action runs the image of its own release. With no
image:input it runsghcr.io/sebhenn/cifail:<version>, built from the same release as the action, so pinning the action to a release tag or a commit SHA pins the container too.@v1moves both with every release. A registry tag can be moved by whoever can push to it, so the strongest pin isimage:with a digest (ghcr.io/sebhenn/cifail@sha256:…). The step prints the digest it ran, and exposes it as theimage-digestoutput. - The container never receives
GITHUB_TOKENin its environment — withcomment: truethe comment is posted from the runner, outside it — but it runs with your workspace mounted at/work, andactions/checkoutv5 and earlier write the job's token into.git/configthere by default. Then whatever image you run can reach whatever that token can, and the action warns when it finds one. (Checkout v6 and later keep the token outside the workspace.) Check out withpersist-credentials: falsebefore this action, and give the job only the permissions it needs:contents: read, pluspull-requests: writeonly if it comments.
Rules are data — YAML with a regex, a title and some prose. cifail never executes anything
from a rule pack, and never runs a command from a log. A malformed regex is skipped rather
than being fatal, and cifail rules validate lints a pack before you trust it.
cifail loads rule packs from the repository you are working in. Alongside
~/.cifail/rules/, it walks up from the working directory looking for .cifail/rules/,
and also honours rules.paths in config.yaml, the CIFAIL_RULES environment variable
and --rules. So running cifail analyze inside a checkout you just cloned will compile
and run that repository's regexes against your log. That is the point of the feature — a
rule like "the determinism contract is broken" belongs in the repo it describes — but it
means a rule pack is code you obtained from someone else, in the sense that matters for
denial of service.
The guard is a time limit: every rule pattern runs under a 2-second match timeout
(RuleEngine.MatchTimeout), and a pattern that exceeds it is skipped with a warning naming
the rule rather than hanging the process. Ecosystem detection has both a per-marker timeout
and an overall budget. Drafts from suggest-rule are additionally compiled under a
1-second timeout before they can be written.
Two things this does not protect against, so read an unfamiliar pack first:
- A rule's
fixtext is advice a human may paste into a terminal. - A rule can be written to match your log and claim whatever it likes about it.
cifail gate deliberately opens no store and makes no network call, so a repo-local pack
influencing a gate verdict cannot also exfiltrate anything.