fini-proof 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +5 -0
- package/README.md +220 -0
- package/action.yml +53 -0
- package/bin/fini-proof.mjs +6 -0
- package/ci/Jenkinsfile +13 -0
- package/ci/Jenkinsfile.daily +23 -0
- package/ci/bitbucket-pipelines-daily.yml +15 -0
- package/ci/bitbucket-pipelines.yml +12 -0
- package/ci/github-actions-daily.yml +31 -0
- package/ci/github-actions-example.yml +14 -0
- package/ci/gitlab-ci-daily.yml +17 -0
- package/ci/gitlab-ci.yml +13 -0
- package/package.json +31 -0
- package/src/cli.mjs +258 -0
- package/src/engines/fail-open.mjs +133 -0
- package/src/engines/gitleaks.mjs +143 -0
- package/src/engines/hollow-tests.mjs +121 -0
- package/src/engines/migrations.mjs +316 -0
- package/src/engines/secrets.mjs +88 -0
- package/src/engines/skip-ratchet.mjs +78 -0
- package/src/engines/tenant-filter.mjs +516 -0
- package/src/engines/vendor/entrypoint-guard-core.mjs +43 -0
- package/src/engines/vendor/migration-invariants-core.mjs +127 -0
- package/src/engines/vendor/secrets-core.mjs +123 -0
- package/src/engines/vendor/vacuous-spec-core.mjs +1066 -0
- package/src/evidence.mjs +130 -0
- package/src/files.mjs +89 -0
- package/src/hooks.mjs +118 -0
- package/src/licence.mjs +85 -0
- package/src/mcp.mjs +230 -0
- package/src/report.mjs +73 -0
- package/src/runner.mjs +306 -0
- package/src/state.mjs +189 -0
- package/src/upload.mjs +85 -0
- package/src/usage.mjs +64 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
Copyright (c) 2026 Finipe Ventures. All rights reserved.
|
|
2
|
+
|
|
3
|
+
Fini Proof is proprietary software. Use requires a valid Fini Proof licence key or an active
|
|
4
|
+
trial (1 repository, 14 days). No right to copy, modify, redistribute or sublicense is granted
|
|
5
|
+
except under a written agreement with Finipe Ventures.
|
package/README.md
ADDED
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
# Fini Proof — CLI and CI step (v0.1)
|
|
2
|
+
|
|
3
|
+
> **Product SSOT:** [docs/FINI_PROOF_SSOT_2026-09-28.md](docs/FINI_PROOF_SSOT_2026-09-28.md) — the architecture decision every other document is downstream of.
|
|
4
|
+
|
|
5
|
+
Fini Proof checks a code change and gives one verdict: **PASS**, **FAIL** or **NOT_MEASURED**.
|
|
6
|
+
|
|
7
|
+
- Every finding shows the file and line, the rule, and a **proof command** that anyone can run again.
|
|
8
|
+
- Every check has a **negative control**: a planted defect that the check must catch before it is allowed to judge your code. A check that cannot fail is not evidence.
|
|
9
|
+
- The verdict **fails closed**: if a check cannot run (unreadable file, missing base branch, no files to check, bad licence), the verdict is NOT_MEASURED. It is never PASS.
|
|
10
|
+
- Every run writes an **evidence file** (`fini-proof/evidence@2`: run id, commit and git tree hash, engine versions and
|
|
11
|
+
hashes, the result of every negative control, input file hashes, who made the change and who verified it, a digest of
|
|
12
|
+
the whole record) and can write **SARIF** for GitHub code scanning. See [Evidence file](#evidence-file-evidence2).
|
|
13
|
+
|
|
14
|
+
Runs offline. No code leaves the machine. Node.js 20 or newer. No dependencies.
|
|
15
|
+
|
|
16
|
+
## Quick start
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npx fini-proof check # whole repository
|
|
20
|
+
npx fini-proof check --base origin/main # only files changed against main (for pull requests)
|
|
21
|
+
npx fini-proof check --format sarif > fini-proof.sarif
|
|
22
|
+
npx fini-proof check --fail-on medium # stricter (default: high)
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Until the package is published to npm, run it from a checkout: `node /path/to/fini-proof/bin/fini-proof.mjs check`.
|
|
26
|
+
|
|
27
|
+
Exit codes: `0` PASS · `1` FAIL · `2` usage error · `3` NOT_MEASURED · `4` licence refused.
|
|
28
|
+
|
|
29
|
+
## The checks (engines)
|
|
30
|
+
|
|
31
|
+
| Engine | What it catches | Languages | Where it came from |
|
|
32
|
+
|---|---|---|---|
|
|
33
|
+
| `hollow-tests` | Tests that cannot fail: no assertion, only constant assertions (`expect(true).toBe(true)`, `assert.ok(true)`, `assert True`), swallowed catch, un-awaited promise, `done()` too early, mock-only / type-only / key-only assertions | JS/TS (Jest, Vitest, Mocha, node:test); Python (pytest, unittest) | Ported from Finipe's F4 vacuous-spec detector (`scripts/ci/check-vacuous-spec.mjs`) |
|
|
34
|
+
| `secrets` | AWS keys, private keys, Slack/Stripe/SendGrid/GitHub/Google/LLM API keys, JWTs in config, real values in `.env` files, hard-coded passwords | any text file | Ported from Finipe's secrets gate (`scripts/ci/check-secrets-and-log-redaction.mjs`) |
|
|
35
|
+
| `fail-open` | CI steps and scripts that report success when they failed: `\|\| true`, `set +e`, `continue-on-error`, `allow_failure`, `catch { process.exit(0) }`, empty catch, `except: pass`, broken ESM entry-point guards | CI YAML, shell, package.json, JS/TS/Python scripts | Entry-point rule ported from Finipe's `check-entrypoint-guard.mjs`; the rest generalised |
|
|
36
|
+
| `skip-ratchet` | `.only` / `fit` (always), and skipped tests above the committed baseline | JS/TS, Python | Skip shapes + `SKIP-LEDGER:` convention from Finipe F4; ratchet from `fini-rule-ratchet.mjs` |
|
|
37
|
+
| `tenant-filter` | A query on a multi-tenant table with no tenant filter, which leaks data across tenants (IDOR) | Raw SQL in any language; TypeORM, Prisma, Sequelize-style, Knex, Django ORM, SQLAlchemy | Detector shapes from Finipe's `.github/scripts/check-tenant-scoping.js`; schema-aware table detection is new |
|
|
38
|
+
| `migrations` | `DROP TABLE` / `DROP COLUMN`, column type changes, `ADD COLUMN … NOT NULL` without a default, `SET NOT NULL`, missing or incomplete down / rollback | SQL (plain, Flyway, golang-migrate, goose, dbmate), Prisma, TypeORM, Knex, Alembic, Django | Loop expansion + down() rules from Finipe's `scripts/ci/lib/migration-invariants.cjs`; destructive-change rules are new |
|
|
39
|
+
|
|
40
|
+
`tenant-filter` and `migrations` are **NOT_APPLICABLE** (they do not block) in a repository with no migrations or no
|
|
41
|
+
table that has the tenant column. Each case is recorded with the reason.
|
|
42
|
+
|
|
43
|
+
### Orchestrated scanner: gitleaks
|
|
44
|
+
|
|
45
|
+
If a `gitleaks` binary is already on `PATH`, Fini Proof runs it as the engine `gitleaks` and holds it to the same rule
|
|
46
|
+
as its own engines:
|
|
47
|
+
|
|
48
|
+
- Before it judges your code, Fini Proof plants three known-shaped secrets (an AWS key, a GitHub token, a private key)
|
|
49
|
+
in a temporary copy and runs gitleaks on it. If gitleaks misses one, exits with an error, times out or writes a
|
|
50
|
+
report that is not JSON, the engine is **NOT_MEASURED** and the run cannot PASS.
|
|
51
|
+
- gitleaks scans a temporary copy of exactly the files Fini Proof hashed (ignore globs, `--base` and `--only` apply),
|
|
52
|
+
with `--redact`. Findings name the file, line and gitleaks rule; the secret itself is never written anywhere.
|
|
53
|
+
- When gitleaks passed its controls, it is the primary secrets check and the built-in `secrets` engine is
|
|
54
|
+
NOT_APPLICABLE for that run. When gitleaks is not installed, the `gitleaks` engine is NOT_APPLICABLE (with the reason)
|
|
55
|
+
and the built-in `secrets` engine runs as before.
|
|
56
|
+
- Fini Proof never downloads a scanner. `FINI_PROOF_GITLEAKS=<path>` uses a binary outside `PATH` (a path that is not
|
|
57
|
+
executable is NOT_MEASURED); `FINI_PROOF_GITLEAKS=off` keeps the built-in engine. The binary's sha256 and version are
|
|
58
|
+
recorded, and they are part of the engine digest, so a new gitleaks binary counts as a new verifier.
|
|
59
|
+
|
|
60
|
+
Every ported file carries its source path and Finipe commit in its header.
|
|
61
|
+
|
|
62
|
+
## Other commands
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
npx fini-proof prove --engine hollow-tests --file test/a.test.js --line 12 # re-run one finding (exit 1 = still there)
|
|
66
|
+
npx fini-proof state # per-property proof state for a run now
|
|
67
|
+
npx fini-proof state --against .fini-proof/runs/<id>.json # what that run proved, as of the files now
|
|
68
|
+
npx fini-proof evidence verify fini-proof-evidence.json # re-check an evidence file's digest
|
|
69
|
+
npx fini-proof negctl hollow-tests/NC-1 # show the planted defect being caught
|
|
70
|
+
npx fini-proof negctl --all # all negative controls
|
|
71
|
+
npx fini-proof baseline # accept today's skipped tests; the count can only go down
|
|
72
|
+
npx fini-proof usage --export usage.json # local usage meter
|
|
73
|
+
npx fini-proof licence # licence / trial status
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A skipped test is allowed without a baseline when a comment names why: `// SKIP-LEDGER: JIRA-123 reason`.
|
|
77
|
+
|
|
78
|
+
## Evidence file (evidence@2)
|
|
79
|
+
|
|
80
|
+
Every `check` writes one JSON record (`.fini-proof/runs/<run id>.json`, or `--json-out` / `--evidence <file>`).
|
|
81
|
+
`fini-proof/evidence@2` has everything `evidence@1` had, plus:
|
|
82
|
+
|
|
83
|
+
| Field | What it is |
|
|
84
|
+
|---|---|
|
|
85
|
+
| `snapshot.tree_hash` | `git rev-parse HEAD^{tree}`. Not a git repository, or no commit yet: `null` with `status: NOT_MEASURED` and the reason (the per-file hashes still identify what was checked). With uncommitted changes it is the HEAD tree and `dirty: true`. |
|
|
86
|
+
| `findings[].property_id` | `<engine>/<rule>`, e.g. `hollow-tests/NO_ASSERTION`. The unit a proof is about. |
|
|
87
|
+
| `engines[].nc_results[]` | `{ id, expect, caught, ms }` for each negative control that ran before the engine was allowed to scan. |
|
|
88
|
+
| `engines[].scope_files` | the files the engine read (its scan scope plus schema files it needed). A change to any of them makes its proofs STALE. |
|
|
89
|
+
| `attestation.maker` | who made the change: `{ kind, id, model?, session?, source }`. From `FINI_PROOF_MAKER_KIND` (`human` or `agent`), `FINI_PROOF_MAKER_ID`, `FINI_PROOF_MAKER_MODEL`, `FINI_PROOF_MAKER_SESSION`; the Claude Code hook records `agent / claude-code` with the session; the MCP server records the MCP client's name. Otherwise the git author of HEAD with `kind: unknown` — the kind is never guessed. |
|
|
90
|
+
| `attestation.verifier` | `{ tool: fini-proof, version, engine_digests, runner: { host_digest, ci } }`. The host name is stored only as a digest. |
|
|
91
|
+
|
|
92
|
+
The digest (`evidenceDigest`) covers all of this. `fini-proof evidence verify <file>` re-computes it and fails an edited
|
|
93
|
+
file, and a record whose maker is `fini-proof` itself (a verifier cannot attest its own change). Every reader (text,
|
|
94
|
+
SARIF, `state`, `evidence verify`, the PR app) also reads `evidence@1` files from older versions.
|
|
95
|
+
|
|
96
|
+
## Proof state
|
|
97
|
+
|
|
98
|
+
`fini-proof state` gives every property (`<engine>/<rule>`) exactly one of five states:
|
|
99
|
+
|
|
100
|
+
| State | Meaning |
|
|
101
|
+
|---|---|
|
|
102
|
+
| **PROVEN** | The engine passed all its controls, this rule has its own negative control (a planted defect) that was caught and its own positive control (the nearest clean look-alike) that stayed clean, and the rule found nothing in the engine's scope. |
|
|
103
|
+
| **FAILED** | The rule found a medium or high finding. Each one has its proof command. |
|
|
104
|
+
| **NOT_MEASURED** | Nothing could be established. `cause` says why: `engine-not-applicable` (e.g. no migrations in the repository), `engine-not-measured`, `no-scope`, `control-failed`, `missing-negative-control` or `missing-positive-control`. |
|
|
105
|
+
| **STALE** | The record says PROVEN or FAILED, but since then a file in its scope was modified, deleted or added (for a whole-repository run), or the engine changed. Run `check` again. |
|
|
106
|
+
| **ADVISORY** | A low or info rule, such as `TENANT_FILTER_UNPROVEN`. A recommendation. It never blocks and is never shown as proven. |
|
|
107
|
+
|
|
108
|
+
- `fini-proof state` runs the check now (the same licensed path as `check`, and it writes the same evidence file) and
|
|
109
|
+
reports the states of that run.
|
|
110
|
+
- `fini-proof state --against <evidence.json>` runs nothing. It verifies the file's digest (an edited file is refused,
|
|
111
|
+
exit 3), then compares the sha256 of each file in each proof's scope with the files on disk now.
|
|
112
|
+
- Every rule of the six built-in engines and the gitleaks adapter has its own negative and positive control, except
|
|
113
|
+
`skip-ratchet/SKIPPED_TEST`: it is never reported as a finding (an over-baseline skip becomes `SKIP_RATCHET`), so no
|
|
114
|
+
planted input can produce it. It is info-level and shows as ADVISORY.
|
|
115
|
+
- `--format json` prints `fini-proof/state@1`: `{ property, state, reason, ncId, runId, snapshot, engineVersion,
|
|
116
|
+
provenAt, invalidatedBy, … }` for each property. `state` is a report and exits 0; use `check` to gate.
|
|
117
|
+
|
|
118
|
+
## Upload to your Finipe Developer workspace (optional)
|
|
119
|
+
|
|
120
|
+
```bash
|
|
121
|
+
export FINI_PROOF_API_KEY=fpk_… # Developers → Fini Proof → API keys (shown once)
|
|
122
|
+
npx fini-proof check --upload # repository = origin's owner/name; override with --repository acme/api
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
- The evidence file is sent to `POST $FINI_PROOF_API/runs` (default `https://developer.finipe.com/api/v1/fini-proof`)
|
|
126
|
+
with the repository name, the branch and `source` (`ci` when `CI` is set, else `cli`). The server re-checks the
|
|
127
|
+
evidence digest and refuses an edited file.
|
|
128
|
+
- **An upload never changes the verdict or the exit code.** If it fails (no key, revoked key, network), a warning is
|
|
129
|
+
printed on stderr and the local evidence file stays the record.
|
|
130
|
+
- The key is sent only in the `Authorization` header and only over https (plain http is accepted for `localhost`
|
|
131
|
+
only). It is never printed.
|
|
132
|
+
- `--upload-gzip` (or `FINI_PROOF_UPLOAD_GZIP=1`) sends the body with `Content-Encoding: gzip`. It is off by default
|
|
133
|
+
because the workspace must accept it. The evidence is about 156 bytes per checked file (the per-file sha256 and
|
|
134
|
+
the per-engine scope that STALE needs): measured 1.56 MB for 10,000 files and 3.13 MB for 20,000 files. Gzip makes
|
|
135
|
+
those 0.47 MB and 0.93 MB. An HTTP 413 answer prints the evidence size and suggests `--upload-gzip`.
|
|
136
|
+
|
|
137
|
+
## Configuration
|
|
138
|
+
|
|
139
|
+
`.fini-proof.json` in the repository root:
|
|
140
|
+
|
|
141
|
+
```json
|
|
142
|
+
{
|
|
143
|
+
"ignore": ["vendor/**", "test/fixtures/**"],
|
|
144
|
+
"tenant": { "column": "tenant_id", "tables": ["orders"], "exemptTables": ["countries"], "exemptPaths": ["src/platform-admin/**"],
|
|
145
|
+
"helpers": ["buildTeamWhereQuery"], "scopeColumns": ["project_id", "user", "token"] }
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
- `helpers`: a shared where-builder that adds the tenant condition. Calling it counts as the tenant filter.
|
|
150
|
+
- `scopeColumns`: other columns your team treats as scoping. Examples are a parent key whose row is itself
|
|
151
|
+
tenant-scoped (`project_id`), the signed-in user for per-user data (`user`), or a secret link token (`token`). They
|
|
152
|
+
are recorded in the evidence file.
|
|
153
|
+
- Tables with Postgres `ENABLE ROW LEVEL SECURITY` in the schema are isolated by the database, so they are not checked
|
|
154
|
+
at the call site. They are listed as `rlsTables`.
|
|
155
|
+
- A query with no visible tenant condition is **UNPROVEN** (low, non-blocking by default) and not MISSING (high) in
|
|
156
|
+
three cases, with the reason in the message: it is re-keyed from an already-loaded row (`id: invoice.id`), the same
|
|
157
|
+
table was queried with the tenant condition earlier in the function, or the result's tenant column is checked
|
|
158
|
+
straight after the fetch.
|
|
159
|
+
|
|
160
|
+
**Adopting on an existing repo:** run `fini-proof baseline` once. It records the migrations already applied
|
|
161
|
+
(path → sha256). An applied migration that has not changed is history and is not judged again. A new or edited
|
|
162
|
+
migration is judged. Changing the baseline is a gated change: the CI gate fails a PR that changes it unless a human
|
|
163
|
+
approves.
|
|
164
|
+
|
|
165
|
+
Ignored globs are written into the evidence file, so a reviewer can see what was left out. If there is no `tenant`
|
|
166
|
+
block, the column is `tenant_id` (and `tenantId`), and the tenant tables are the ones whose schema (migrations, Prisma,
|
|
167
|
+
TypeORM entities, Django/SQLAlchemy models) declares that column. If you set a `tenant` block and no table matches,
|
|
168
|
+
the result is NOT_MEASURED, not a pass.
|
|
169
|
+
|
|
170
|
+
A deliberate exception is written in the code, next to the line, with a reason that stays there for review:
|
|
171
|
+
`// fini-proof: tenant-exempt platform-admin report reads all tenants (SEC-19)` and
|
|
172
|
+
`-- fini-proof: allow DROP_COLUMN CHG-812 column unused since release 41`.
|
|
173
|
+
|
|
174
|
+
Add `.fini-proof/runs/` to `.gitignore`. Every check writes an evidence file there.
|
|
175
|
+
|
|
176
|
+
## AI coding agents (Claude Code, Codex, any MCP client)
|
|
177
|
+
|
|
178
|
+
The agent that wrote the code does not decide whether it is done.
|
|
179
|
+
|
|
180
|
+
- **MCP server:** `npx fini-proof mcp` gives the agent three tools: `check`, `explain_finding` and `list_rules`.
|
|
181
|
+
Claude Code: `claude mcp add fini-proof -- npx --yes fini-proof@0.3.0 mcp`.
|
|
182
|
+
- **Claude Code hooks:** see `integrations/claude-code/`. The Stop hook exits 2 with the findings while the verdict is
|
|
183
|
+
FAIL, so Claude cannot finish. PostToolUse gives feedback on each file as it is written.
|
|
184
|
+
- **Codex and other agents:** see `integrations/codex/` for an `AGENTS.md` section and a CI gate. The gate also fails
|
|
185
|
+
any pull request that changes the verifier's own config without human approval.
|
|
186
|
+
- `fini-proof check --only <file>` checks just the named files, which is what an editor or agent hook needs.
|
|
187
|
+
|
|
188
|
+
## CI
|
|
189
|
+
|
|
190
|
+
| System | File |
|
|
191
|
+
|---|---|
|
|
192
|
+
| GitHub Actions | `action.yml` (reusable action) — example workflow in `ci/github-actions-example.yml` |
|
|
193
|
+
| GitLab CI | `ci/gitlab-ci.yml` |
|
|
194
|
+
| Jenkins | `ci/Jenkinsfile` |
|
|
195
|
+
| Bitbucket Pipelines | `ci/bitbucket-pipelines.yml` |
|
|
196
|
+
|
|
197
|
+
**Daily run of the default branch.** `ci/github-actions-daily.yml`, `ci/gitlab-ci-daily.yml`, `ci/Jenkinsfile.daily`
|
|
198
|
+
and `ci/bitbucket-pipelines-daily.yml` check the whole default branch once a day and send the evidence to your Finipe
|
|
199
|
+
Developer workspace with `check --upload` (set `FINI_PROOF_API_KEY` as a CI secret). GitHub and Jenkins carry the
|
|
200
|
+
schedule in the file; GitLab and Bitbucket need a schedule created once in their UI (the file says where). A failed
|
|
201
|
+
upload is a warning and never changes the exit code.
|
|
202
|
+
|
|
203
|
+
Each one is one command: `npx --yes fini-proof@0.3.0 check --base origin/<target> --fail-on high --sarif-out fini-proof.sarif --json-out fini-proof-evidence.json`.
|
|
204
|
+
Clone with full history (`fetch-depth: 0`, `GIT_DEPTH: 0`, `depth: full`). A shallow clone gives NOT_MEASURED, not a guessed PASS.
|
|
205
|
+
`test/ci-templates.test.mjs` runs every one of these commands against a real git repository (a clean pull request must pass, a defective one must fail). The daily templates are run the same way on the default branch, uploading to a local stand-in server.
|
|
206
|
+
|
|
207
|
+
## Licence
|
|
208
|
+
|
|
209
|
+
- **Trial:** without a key, Fini Proof runs on 1 repository for 14 days (state in `~/.fini-proof/trial.json`).
|
|
210
|
+
- **Licence key:** an offline, Ed25519-signed key (organisation, seats, expiry). Set `FINI_PROOF_LICENCE` (the key or a path to it), or save it as `.fini-proof/licence.key` or `~/.fini-proof/licence.key`. It is checked on the machine. There is no network call.
|
|
211
|
+
- **Usage meter:** every run adds one line to `~/.fini-proof/usage.jsonl` (commit authors are stored only as hashes). `fini-proof usage --export` writes a summary for billing and seat checks.
|
|
212
|
+
- Vendor only: licence issuing and key handling are in `LICENCE_OPS.md` (`npm run admin -- issue …`, `npm run admin -- keygen --out <dir outside any git checkout>`).
|
|
213
|
+
|
|
214
|
+
## Demo
|
|
215
|
+
|
|
216
|
+
`npm run demo` (add `-- --pause` to stop between steps). See `SALES_DEMO.md`.
|
|
217
|
+
|
|
218
|
+
## Tests
|
|
219
|
+
|
|
220
|
+
`npm test` (node:test, about 10 seconds, no build).
|
package/action.yml
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: Fini Proof
|
|
2
|
+
description: Proof-based code verification — every finding has a runnable proof, every check a negative control, and a check that cannot run is NOT_MEASURED, never PASS.
|
|
3
|
+
branding: { icon: check-circle, color: green }
|
|
4
|
+
inputs:
|
|
5
|
+
base:
|
|
6
|
+
description: Base ref to diff against (default for pull requests is the PR base branch). Empty = scan the whole repository.
|
|
7
|
+
required: false
|
|
8
|
+
default: ''
|
|
9
|
+
fail-on:
|
|
10
|
+
description: Lowest severity that fails the job (high | medium | low | none).
|
|
11
|
+
required: false
|
|
12
|
+
default: high
|
|
13
|
+
licence:
|
|
14
|
+
description: Fini Proof licence key (pass a secret, e.g. secrets.FINI_PROOF_LICENCE). Empty = trial mode.
|
|
15
|
+
required: false
|
|
16
|
+
default: ''
|
|
17
|
+
upload-sarif:
|
|
18
|
+
description: Upload SARIF to GitHub code scanning (needs security-events write).
|
|
19
|
+
required: false
|
|
20
|
+
default: 'true'
|
|
21
|
+
outputs:
|
|
22
|
+
verdict:
|
|
23
|
+
description: PASS, FAIL or NOT_MEASURED
|
|
24
|
+
value: ${{ steps.check.outputs.verdict }}
|
|
25
|
+
runs:
|
|
26
|
+
using: composite
|
|
27
|
+
steps:
|
|
28
|
+
- uses: actions/setup-node@v4
|
|
29
|
+
with: { node-version: '20' }
|
|
30
|
+
- id: check
|
|
31
|
+
shell: bash
|
|
32
|
+
env:
|
|
33
|
+
FINI_PROOF_LICENCE: ${{ inputs.licence }}
|
|
34
|
+
FINI_PROOF_HOME: ${{ runner.temp }}/fini-proof-home
|
|
35
|
+
INPUT_BASE: ${{ inputs.base || (github.event_name == 'pull_request' && format('origin/{0}', github.base_ref) || '') }}
|
|
36
|
+
INPUT_FAIL_ON: ${{ inputs.fail-on }}
|
|
37
|
+
run: |
|
|
38
|
+
set -euo pipefail
|
|
39
|
+
args=(check --fail-on "$INPUT_FAIL_ON" --sarif-out fini-proof.sarif --json-out fini-proof-evidence.json)
|
|
40
|
+
if [ -n "$INPUT_BASE" ]; then args+=(--base "$INPUT_BASE"); fi
|
|
41
|
+
rc=0
|
|
42
|
+
node "${{ github.action_path }}/bin/fini-proof.mjs" "${args[@]}" || rc=$?
|
|
43
|
+
case "$rc" in 0) v=PASS;; 1) v=FAIL;; *) v=NOT_MEASURED;; esac
|
|
44
|
+
echo "verdict=$v" >> "$GITHUB_OUTPUT"
|
|
45
|
+
echo "rc=$rc" >> "$GITHUB_OUTPUT"
|
|
46
|
+
- if: always() && inputs.upload-sarif == 'true' && hashFiles('fini-proof.sarif') != ''
|
|
47
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
48
|
+
with: { sarif_file: fini-proof.sarif, category: fini-proof }
|
|
49
|
+
- if: always()
|
|
50
|
+
uses: actions/upload-artifact@v4
|
|
51
|
+
with: { name: fini-proof-evidence, path: fini-proof-evidence.json, if-no-files-found: ignore }
|
|
52
|
+
- shell: bash
|
|
53
|
+
run: exit ${{ steps.check.outputs.rc }}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Entry point. Always runs main() (no argv[1] self-compare — that guard shape is exactly what the
|
|
3
|
+
// fail-open engine reports; a bin that only runs main is the safe form through npx symlinks).
|
|
4
|
+
// main() returns an exit code, or a promise of one for the long-running commands (mcp, hook).
|
|
5
|
+
import { main } from '../src/cli.mjs'
|
|
6
|
+
Promise.resolve(main(process.argv.slice(2))).then((rc) => { process.exitCode = rc }, (e) => { process.stderr.write(`fini-proof: ${e.stack || e.message}\n`); process.exitCode = 2 })
|
package/ci/Jenkinsfile
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Jenkinsfile snippet — add this stage. FINI_PROOF_LICENCE is a Jenkins "secret text" credential.
|
|
2
|
+
stage('Fini Proof') {
|
|
3
|
+
environment {
|
|
4
|
+
FINI_PROOF_LICENCE = credentials('fini-proof-licence')
|
|
5
|
+
FINI_PROOF_HOME = "${env.WORKSPACE}/.fini-proof-home"
|
|
6
|
+
}
|
|
7
|
+
steps {
|
|
8
|
+
sh 'npx --yes fini-proof@0.3.0 check --base "origin/${CHANGE_TARGET:-main}" --fail-on high --sarif-out fini-proof.sarif --json-out fini-proof-evidence.json'
|
|
9
|
+
}
|
|
10
|
+
post {
|
|
11
|
+
always { archiveArtifacts artifacts: 'fini-proof.sarif, fini-proof-evidence.json', allowEmptyArchive: true }
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Jenkinsfile — daily run of the whole default branch, evidence sent to your Finipe Developer workspace (--upload).
|
|
2
|
+
// Use it as the pipeline script of a job on the default branch. Credentials ("secret text"): fini-proof-api-key and
|
|
3
|
+
// fini-proof-licence. Only the evidence file is sent (rule ids, file:line, hashes) — never source code.
|
|
4
|
+
pipeline {
|
|
5
|
+
agent any
|
|
6
|
+
triggers { cron('H 2 * * *') } // daily; H spreads the start minute
|
|
7
|
+
environment {
|
|
8
|
+
FINI_PROOF_API_KEY = credentials('fini-proof-api-key')
|
|
9
|
+
FINI_PROOF_LICENCE = credentials('fini-proof-licence')
|
|
10
|
+
FINI_PROOF_HOME = "${env.WORKSPACE}/.fini-proof-home"
|
|
11
|
+
}
|
|
12
|
+
stages {
|
|
13
|
+
stage('Fini Proof (daily)') {
|
|
14
|
+
when { anyOf { branch 'main'; branch 'master' } }
|
|
15
|
+
steps {
|
|
16
|
+
sh 'npx --yes fini-proof@0.3.0 check --upload --fail-on high --sarif-out fini-proof.sarif --json-out fini-proof-evidence.json'
|
|
17
|
+
}
|
|
18
|
+
post {
|
|
19
|
+
always { archiveArtifacts artifacts: 'fini-proof.sarif, fini-proof-evidence.json', allowEmptyArchive: true }
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Bitbucket Pipelines — daily run of the whole default branch, evidence sent to your Finipe Developer workspace.
|
|
2
|
+
# Add this custom pipeline, then Repository settings → Schedules → New schedule: branch = the default branch,
|
|
3
|
+
# pipeline = custom: fini-proof-daily, interval = Daily. Set FINI_PROOF_API_KEY and FINI_PROOF_LICENCE as secured
|
|
4
|
+
# repository variables. Only the evidence file is sent (rule ids, file:line, hashes) — never source code.
|
|
5
|
+
pipelines:
|
|
6
|
+
custom:
|
|
7
|
+
fini-proof-daily:
|
|
8
|
+
- step:
|
|
9
|
+
name: Fini Proof (daily)
|
|
10
|
+
image: node:20
|
|
11
|
+
clone: { depth: full }
|
|
12
|
+
script:
|
|
13
|
+
- export FINI_PROOF_HOME="$BITBUCKET_CLONE_DIR/.fini-proof-home"
|
|
14
|
+
- npx --yes fini-proof@0.3.0 check --upload --fail-on high --sarif-out fini-proof.sarif --json-out fini-proof-evidence.json
|
|
15
|
+
artifacts: [fini-proof.sarif, fini-proof-evidence.json]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Bitbucket Pipelines — add this step. Set FINI_PROOF_LICENCE as a secured repository variable.
|
|
2
|
+
pipelines:
|
|
3
|
+
pull-requests:
|
|
4
|
+
'**':
|
|
5
|
+
- step:
|
|
6
|
+
name: Fini Proof
|
|
7
|
+
image: node:20
|
|
8
|
+
clone: { depth: full }
|
|
9
|
+
script:
|
|
10
|
+
- export FINI_PROOF_HOME="$BITBUCKET_CLONE_DIR/.fini-proof-home"
|
|
11
|
+
- npx --yes fini-proof@0.3.0 check --base "origin/${BITBUCKET_PR_DESTINATION_BRANCH:-main}" --fail-on high --sarif-out fini-proof.sarif --json-out fini-proof-evidence.json
|
|
12
|
+
artifacts: [fini-proof.sarif, fini-proof-evidence.json]
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# .github/workflows/fini-proof-daily.yml — once a day, the whole default branch is checked and the evidence is sent to
|
|
2
|
+
# your Finipe Developer workspace (`--upload`), where proof state (PROVEN / FAILED / NOT_MEASURED / STALE / ADVISORY)
|
|
3
|
+
# and the daily digest are kept. Only the evidence file is sent (rule ids, file:line, hashes) — never source code.
|
|
4
|
+
# GitHub runs `schedule` on the default branch. Secrets: FINI_PROOF_API_KEY (Developers → Fini Proof → API keys) and
|
|
5
|
+
# FINI_PROOF_LICENCE. The pull-request check stays in github-actions-example.yml.
|
|
6
|
+
name: fini-proof-daily
|
|
7
|
+
on:
|
|
8
|
+
schedule:
|
|
9
|
+
- cron: '30 2 * * *' # daily 02:30 UTC (08:00 IST)
|
|
10
|
+
workflow_dispatch: {}
|
|
11
|
+
permissions: { contents: read, security-events: write }
|
|
12
|
+
jobs:
|
|
13
|
+
proof:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v4
|
|
17
|
+
with: { fetch-depth: 0 }
|
|
18
|
+
- uses: actions/setup-node@v4
|
|
19
|
+
with: { node-version: '20' }
|
|
20
|
+
- name: Fini Proof — default branch, daily
|
|
21
|
+
env:
|
|
22
|
+
FINI_PROOF_API_KEY: ${{ secrets.FINI_PROOF_API_KEY }}
|
|
23
|
+
FINI_PROOF_LICENCE: ${{ secrets.FINI_PROOF_LICENCE }}
|
|
24
|
+
FINI_PROOF_HOME: ${{ runner.temp }}/fini-proof-home
|
|
25
|
+
run: npx --yes fini-proof@0.3.0 check --upload --fail-on high --sarif-out fini-proof.sarif --json-out fini-proof-evidence.json
|
|
26
|
+
- if: always() && hashFiles('fini-proof.sarif') != ''
|
|
27
|
+
uses: github/codeql-action/upload-sarif@v3
|
|
28
|
+
with: { sarif_file: fini-proof.sarif, category: fini-proof-daily }
|
|
29
|
+
- if: always()
|
|
30
|
+
uses: actions/upload-artifact@v4
|
|
31
|
+
with: { name: fini-proof-daily-evidence, path: fini-proof-evidence.json, if-no-files-found: ignore }
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# .github/workflows/fini-proof.yml — one step. fetch-depth: 0 so --base can find the merge-base
|
|
2
|
+
# (a shallow clone gives NOT_MEASURED, never a guessed PASS).
|
|
3
|
+
name: fini-proof
|
|
4
|
+
on: [pull_request, push]
|
|
5
|
+
permissions: { contents: read, security-events: write }
|
|
6
|
+
jobs:
|
|
7
|
+
proof:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
with: { fetch-depth: 0 }
|
|
12
|
+
- uses: finipe/fini-proof@v0.3.0
|
|
13
|
+
with:
|
|
14
|
+
licence: ${{ secrets.FINI_PROOF_LICENCE }}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# GitLab CI — daily run of the whole default branch, evidence sent to your Finipe Developer workspace (`--upload`).
|
|
2
|
+
# Create the schedule once: CI/CD → Schedules → New schedule, interval "30 2 * * *" (daily), target branch = the
|
|
3
|
+
# default branch. Set FINI_PROOF_API_KEY and FINI_PROOF_LICENCE as masked CI/CD variables. Only the evidence file is
|
|
4
|
+
# sent (rule ids, file:line, hashes) — never source code. The merge-request job stays in gitlab-ci.yml.
|
|
5
|
+
fini-proof-daily:
|
|
6
|
+
image: node:20
|
|
7
|
+
stage: test
|
|
8
|
+
rules:
|
|
9
|
+
- if: '$CI_PIPELINE_SOURCE == "schedule" && $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH'
|
|
10
|
+
variables:
|
|
11
|
+
GIT_DEPTH: "0"
|
|
12
|
+
FINI_PROOF_HOME: "$CI_PROJECT_DIR/.fini-proof-home"
|
|
13
|
+
script:
|
|
14
|
+
- npx --yes fini-proof@0.3.0 check --upload --fail-on high --sarif-out fini-proof.sarif --json-out fini-proof-evidence.json
|
|
15
|
+
artifacts:
|
|
16
|
+
when: always
|
|
17
|
+
paths: [fini-proof.sarif, fini-proof-evidence.json]
|
package/ci/gitlab-ci.yml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# GitLab CI — include this file, or paste the job. Evidence JSON is kept as an artifact; SAST report via SARIF
|
|
2
|
+
# is attached for download (GitLab code-quality import of SARIF is not native).
|
|
3
|
+
fini-proof:
|
|
4
|
+
image: node:20
|
|
5
|
+
stage: test
|
|
6
|
+
variables:
|
|
7
|
+
GIT_DEPTH: "0"
|
|
8
|
+
FINI_PROOF_HOME: "$CI_PROJECT_DIR/.fini-proof-home"
|
|
9
|
+
script:
|
|
10
|
+
- npx --yes fini-proof@0.3.0 check --base "origin/${CI_MERGE_REQUEST_TARGET_BRANCH_NAME:-$CI_DEFAULT_BRANCH}" --fail-on high --sarif-out fini-proof.sarif --json-out fini-proof-evidence.json
|
|
11
|
+
artifacts:
|
|
12
|
+
when: always
|
|
13
|
+
paths: [fini-proof.sarif, fini-proof-evidence.json]
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fini-proof",
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "Proof-based code verification: every finding carries a runnable proof, every check has a negative control, and a check that cannot run is NOT_MEASURED, never PASS.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"fini-proof": "bin/fini-proof.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"src",
|
|
12
|
+
"action.yml",
|
|
13
|
+
"ci",
|
|
14
|
+
"README.md"
|
|
15
|
+
],
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=20"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"test": "node --test test/*.test.mjs integrations/*/*.test.mjs",
|
|
21
|
+
"demo": "node scripts/demo.mjs",
|
|
22
|
+
"selfcheck": "node bin/fini-proof.mjs check",
|
|
23
|
+
"admin": "node scripts/fini-proof-admin.mjs",
|
|
24
|
+
"smoke:github": "bash scripts/smoke-github.sh",
|
|
25
|
+
"tokens:sync": "node scripts/sync-finipe-tokens.mjs",
|
|
26
|
+
"tokens:check": "node scripts/sync-finipe-tokens.mjs --check"
|
|
27
|
+
},
|
|
28
|
+
"license": "SEE LICENSE IN LICENSE",
|
|
29
|
+
"author": "Finipe Ventures",
|
|
30
|
+
"dependencies": {}
|
|
31
|
+
}
|