Metadata-Version: 2.4
Name: fortifie
Version: 0.2.0
Summary: Fortifie CLI: scan and harden a build you own, from your terminal or CI.
Author-email: Fortifie <founder@fortifie.xyz>
License: Proprietary
Project-URL: Homepage, https://fortifie.xyz/start
Project-URL: Documentation, https://fortifie.xyz/cli/
Project-URL: Issues, https://fortifie.xyz/contact
Keywords: fortifie,cli,ci,hardening
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"

# fortifie: scan and harden from your terminal or CI

Fortifie is one product with three doors: upload a file on the website, call it
from your editor (MCP), or call it from this CLI / your CI pipeline. All three
use the same Fortifie account and the same API key.

This package is the CLI. It talks to the hosted hardening service at
`https://fortifie-mcp.fly.dev`. The engine stays server-side: you upload a build
you own, the server returns findings (and, on a successful harden, a hardened
binary plus a per-run audit). Technique names are never printed.

**Self-serve protection today is JavaScript and macOS.** Other platforms are not
a self-serve upload path yet. The build is uploaded, never launched, and deleted
after the run.

## Install

The CLI is not on PyPI yet. Install the published wheel:

```bash
python3 -m pip install https://fortifie.xyz/cli/fortifie-0.2.0-py3-none-any.whl
fortifie --version    # fortifie 0.2.0
```

To verify the file before installing:

```bash
curl -fsSL -o fortifie-0.2.0-py3-none-any.whl \
  https://fortifie.xyz/cli/fortifie-0.2.0-py3-none-any.whl
curl -fsSL https://fortifie.xyz/cli/SHA256SUMS | sha256sum -c -
python3 -m pip install ./fortifie-0.2.0-py3-none-any.whl
```

Requires Python 3.9+. No third-party runtime dependencies.

From a checkout of this repo: `pip install .`

`pip install fortifie` will work once the package is published to PyPI. That
step needs a PyPI token; see `PUBLISH.md`.

## Connect your account

MCP and CLI calls need a Solo or Studio API key. Free accounts cannot issue
keys. The key is never printed by the CLI.

```bash
# Recommended: sign in with a browser code (same idea as `gh auth login`)
fortifie login
# Opens https://fortifie.xyz/account/connect, you enter a short code,
# the CLI stores the key at ~/.config/fortifie/credentials (mode 600).

fortifie whoami          # plan + quota; does not print the key
fortifie logout          # removes the saved key
```

Or create a key at https://fortifie.xyz/account/keys and provide it yourself:

```bash
export FORTIFIE_API_KEY="ftk_…"     # preferred over --api-key in shell history
# or: fortifie --api-key "ftk_…" …
```

Resolution order: `--api-key`, then `$FORTIFIE_API_KEY`, then the saved
credentials file.

Default server: `https://fortifie-mcp.fly.dev` (override with `--server` or
`$FORTIFIE_SERVER`).

## Commands

```bash
fortifie login                         # connect this machine to your account
fortifie logout
fortifie whoami                        # plan + harden-run quota (never prints the key)
fortifie assess <artifact>             # findings summary + recommended tier
fortifie harden <artifact> [--out PATH] [--audit-out PATH]
                                       # upload, poll, verify, download
fortifie ci-release <artifact>         # assess then harden; fail-closed
fortifie resign <artifact> --identity ID --notary-profile PROFILE
                                       # re-sign + notarize a returned Mac app
fortifie status <run_id>               # current run state
fortifie audit  <run_id> [--json]      # fetch the audit JSON for a run
```

`harden` and `ci-release` also need an authorization record (who authorized
hardening this artifact; recorded verbatim in the audit):

```bash
export FORTIFIE_AUTHORIZED="Acme Inc, 2026-11-13"   # or --authorized "…"
```

Prefer the shape `Name or Org, YYYY-MM-DD`. Missing it fails client-side before
spending a metered harden run. `assess` is read-only and is not a metered harden.

Honest current limit: `fortifie assess fixture://…` works against the hosted
server. `fortifie assess ./file.js` (a raw upload) is not a fresh scan on the
hosted `/v1/assess` path yet — the CLI fails closed with a clear error rather
than pretending it worked. Scan a JavaScript file or a Mac app on the website
for now, or `fortifie harden` a JavaScript file (that upload path is live).
Mach-O / `.app` harden from the CLI still needs a recorded scan ref; the
website job queue is the self-serve path for those.


### Re-sign a returned Mac app

Fortifie hardens a Mach-O or zipped `.app` and returns it with only a temporary
ad-hoc signature. Before you distribute the build, re-sign it with your own
Developer ID certificate and notarize it. This helper runs the standard Apple
commands (`codesign --deep --force --options runtime --timestamp`, then
`notarytool submit --wait` and `stapler staple`). Fortifie never holds your
Developer ID identity.

```bash
fortifie resign ./MyApp.app \
  --identity "Developer ID Application: Your Name (TEAMID)" \
  --notary-profile your-notarytool-profile \
  --dry-run          # print the commands; add no flag to run them
```

`--identity -` is for local testing only: it ad-hoc signs and prints, but does
not submit, the notarize steps.

### `harden` / `ci-release` options

| Flag | Meaning |
|---|---|
| `--out PATH` | where to write the hardened artifact (default `<name>.hardened`) |
| `--audit-out PATH` | where to write the audit JSON |
| `--authorized` | **required** authorization record (or `FORTIFIE_AUTHORIZED`) |
| `--tier`, `--profile`, `--target-runtime` | request tier / scan profile / runtime |
| `--timeout SECONDS` | overall poll budget (default 600) |
| `--poll-interval SECONDS` | seconds between status polls (default 3) |
| `--fail-on-needs-human` | **(default)** fail the step on `needs_human_review` |
| `--warn-on-needs-human` | warn instead of fail; still writes **no** artifact |

### Exit codes (branch on these in CI)

| Code | Meaning |
|---|---|
| 0 | success (or warn-mode needs-human) |
| 2 | config/usage error (missing auth, bad args, artifact not found) |
| 3 | `needs_human_review` (default fail-safe) |
| 4 | run timed out |
| 5 | server/network error |
| 6 | artifact integrity check failed (sha256 mismatch) |
| 7 | terminal with no hardened binary (`completed_plan_only`) |

## The fail-safe

A run can come back **`needs_human_review`** when the loop is not confident
enough to ship a build unsupervised. When that happens the CLI will **never**
write a hardened artifact and will **never** let the build look shipped:

- **Default (`--fail-on-needs-human`)**: the step **fails** (exit 3). Review the
  run at https://fortifie.xyz/account/jobs. No artifact is written.
- **`--warn-on-needs-human`**: the step **passes** (exit 0) with a warning, but
  **still writes no hardened artifact**, so a later deploy stage has nothing to
  publish.

On the success path (`completed`) the CLI also verifies the downloaded
artifact's sha256 against the hash in the audit response when one is provided; a
mismatch fails (exit 6) and writes nothing.

Only `completed` ever yields a hardened binary:

| State | CLI behavior |
|---|---|
| `completed` | download + verify + write hardened artifact and audit (exit 0) |
| `completed_plan_only` | plan + evidence only, NO binary; writes the audit, fails exit 7 |
| `needs_human_review` | fail exit 3 (or warn), NO binary |
| `blocked_unauthorized` | fail exit 2, NO binary |
| `error` | fail exit 5, NO binary |

`fetch_audit` also returns a method-free `client_summary` that is safe to paste
into CI logs or dashboards.

## GitHub Actions

Copy `github-action/example-release-workflow.yml` into
`.github/workflows/release-harden.yml`. It installs the public wheel, then runs
`fortifie ci-release`. The important part:

```yaml
- name: Install Fortifie CLI
  run: python3 -m pip install https://fortifie.xyz/cli/fortifie-0.2.0-py3-none-any.whl

- name: Fortifie assess + harden
  env:
    FORTIFIE_API_KEY: ${{ secrets.FORTIFIE_API_KEY }}
    FORTIFIE_AUTHORIZED: "${{ github.repository }}, ${{ steps.today.outputs.date }}"
  run: |
    fortifie ci-release dist/app.js \
      --out dist/app.js.hardened \
      --fail-on-needs-human

- name: Publish hardened build
  if: ${{ hashFiles('dist/app.js.hardened') != '' }}
  run: ./scripts/publish.sh dist/app.js.hardened
```

Store `FORTIFIE_API_KEY` as a repository secret. The job should also attach the
audit JSON even when the harden fails (see the example workflow).

An in-repo composite action lives at `github-action/` for this repository. It is
**not** published to the GitHub Marketplace, so a customer workflow should not
use `uses: fortifie/harden-action@v1`.

## GitLab CI

See `gitlab/.gitlab-ci.yml`. It builds, hardens in a gated stage (`artifacts:
when: always` attaches the audit even on failure), and only deploys when the
hardened artifact exists. Store `FORTIFIE_API_KEY` as a masked, protected
CI/CD variable.

## CI logs

The CLI prints findings, severities, counts, tiers, and run ids only. No
protection technique names appear in output, help text, or the audit summary it
prints, so CI logs stay safe to share. The API key is never printed.

## Testing

```bash
python3 -m unittest discover -s tests -p 'test_*.py' -v   # unit + e2e against a fake MCP
./dogfood/run_dogfood.sh                                 # full flow against an owned fixture
```

`tests/fake_server.py` is a small stand-in implementing the documented MCP tool
shapes (`assess`, `harden`, `status`, `fetch_audit`, `usage`). It does **no**
real hardening; it returns a real owned-fixture audit JSON so the downstream
shapes and the fail-safe are exercised honestly.

## Server contract

This CLI codes against `spec/SPEC.md` (SPEC v1). Default base URL:
`https://fortifie-mcp.fly.dev`.

```
POST /v1/assess          multipart artifact -> {run_id, findings_summary, recommended_tier, risk_text}
POST /v1/harden          multipart artifact + options(json, incl. required "authorized") -> {run_id, state}
GET  /v1/status/<run_id> -> {state, progress, needs_human, reason}
GET  /v1/audit/<run_id>  -> {audit, hardened_artifact_ref|null, client_summary}
GET  /v1/artifact/<run_id> -> raw hardened binary (resolves the opaque hardened_artifact_ref)
GET  /v1/usage           -> plan + harden-run quota for this key
```

Audit JSON is `fortifie.agentic.run/v1`. A harden run is a single metered event.
