Skip to content
Merged
96 changes: 96 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,102 @@ schemas and the `paramify` CLI — not the internal code.

## [Unreleased]

### Added

- `paramify programs` — a new command group over the Paramify workspace.
`programs list` shows each program's readable name next to its project UUID;
`programs target` selects programs (interactively, by name/id, or `--all`) and
writes them as fanout targets, filling in the shared config they need. The API
identifies programs by UUID while people know them by name; this closes that
gap without anyone copying a UUID by hand. The shared values (`--cert-uri`,
`--report-from`) are shown on every interactive run with what the manifest
holds today as the prompt default and a note of where it comes from: enter
keeps it and writes nothing, typing over it updates the category value. So the
same command adds a program and rolls the report window forward, and neither
requires opening the manifest to see what the next run will carry. Entries that
resolve to different values get no default — either one offered as *the* answer
would misreport the other.
- `program_name` — an optional target field on the Paramify VER fetchers. The
fetcher uses it for its evidence filename and the uploader for the artifact
title, so per-program artifacts read as `… - Alpha Cloud Services` rather than
a bare UUID. A UUID prefix stays in the filename because program names are not
guaranteed unique.

### Changed

- The `tui` extra pins `textual>=8,<9` (was `>=1.0,<2.0`). The old range was not
what anyone ran, and focus / `Input` behaviour differs enough across those lines
that the TUI is not the same app on 1.x. `tests/test_tui_keys.py` (new) drives
the real app through Textual's pilot to hold the key-and-focus contract: what
each tab focuses, that the globals survive a repeat tab press, and that enter
reaches an action wherever the footer says it does.
- **TUI**: the footer hint bar lists `esc` (the only way out of a focused text
field back to the shortcut keys — an `Input` consumes every printable key) and
the Run tab shows `enter/ctrl+r`, since focus opens on the ▶ Run button and
`enter` presses it.
- **Paramify VER fetchers**: `report_from` / `report_to` / `api_base_url` /
`http_timeout` moved out of `secrets[]`. Every declared secret is mandatory, so
declaring optional knobs there made them required, contradicting their
documented defaults. `cert_package_uri`, `api_base_url` and `http_timeout` are
now category config (`fetchers/_categories/paramify.yaml`) — one value per
workspace, set once under `platforms.paramify.config` instead of copied onto
every target.
- Each VER report's `_summary` now carries a `collection` block (status + the
API-failure ledger). `/issues` is the only call these fetchers make, so a
failure yields empty report arrays; without this a failed report was
indistinguishable from a genuinely clean one to anything reading the payload.
- The uploader prefers a target's `program_name` over its opaque id when titling
an artifact. Fetchers whose id is already readable are unaffected.
- **Every timestamp in a VER report is now emitted in one format** — UTC, second
precision, literal `Z` (`2026-07-30T09:00:00Z`). Values from the Paramify API
(`detectedAt`, `evaluationCompletedAt`, the `dueDate` quoted in an overdue
explanation) were previously passed through with the API's millisecond
precision, so a single document mixed notations; they are normalized on the way
in, and non-UTC offsets are converted rather than preserved. A `report_from` /
`report_to` given as a bare date is expanded, with a date-only end reported as
that day's last second (`2026-06-30` → `2026-06-30T23:59:59Z`) to match the
window actually collected.

### Fixed

- **TUI**: pressing the number of the tab you are already on no longer clears
focus. Assigning `TabbedContent.active` the value it already holds fires no
`TabActivated`, so nothing re-homed focus after it was cleared — and because a
page's bindings only resolve while focus is inside that page, every page
shortcut (`a`/`e`/`x`, `ctrl+r`, `j`/`k`, the arrows) silently went dead until
you pressed escape or a different tab.
- **TUI**: `ctrl+p` on the Paramify tab runs Preview instead of opening Textual's
command palette, which claims that key as a *priority* binding — checked ahead
of the focused widget, so the page's own binding could never fire. `p` now does
it too, mirroring the Manifest tab's preview key.
- **TUI**: `enter` does what the footer promises on the two tables where it did
nothing at all — on a run it drills into that run's evidence files (where enter
opens one), and on a manifest row it opens the entry editor.
- **TUI**: editing the manifest's output dir no longer loses the path. Textual
selects an `Input`'s value on focus, so the first keystroke replaced the whole
path; and an edit never submitted with `enter` was silently reverted by the next
rebuild. Focus no longer selects the value, and leaving the field commits it.
- **TUI**: `enter` in a confirmation dialog now means No. Yes is composed first,
so it took the default focus — on the dialogs that delete a manifest file,
remove an entry, and upload to Paramify. `y` still confirms.
- **TUI**: config set at the category level showed as unset on every entry that
inherited it — the manifest screen read only the entry's own `config` block and
had no notion of `platforms.<category>.config`. Both the detail pane and the
summary count now render `api.effective_config()`, the same merge the runner
performs, and show which layer each value came from.
- **Paramify VER fetchers**: a pending or rejected `RISK_ADJUSTMENT` no longer
reports `finalDisposition: "Partially Mitigated"` — mitigation now requires an
accepted deviation, not an unapproved request.
- An issue carrying neither `poamId` nor `id` no longer raises `KeyError` and
kills the whole report.
- `PARAMIFY_HTTP_TIMEOUT` is parsed at call time and falls back to the default on
a malformed value, instead of aborting the run with a bare `ValueError` at
import.
- A timestamped `report_to` no longer over-includes up to a day beyond the
declared reporting period.
- `PARAMIFY_REPORT_TO` is now declared, so it can actually be set through a
manifest (the runner passes only declared env vars).

## [0.3.1-beta] - 2026-07-28

### Changed
Expand Down
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,9 +162,42 @@ paramify run <manifest> # run it
paramify runs # past runs under an output dir (newest first)
paramify evidence <file> # read one evidence file (normalizing the envelope)
paramify upload [run-dir] # push a run's evidence to Paramify (default: latest run)
paramify programs <sub> # list workspace programs; turn them into targets
paramify manifest <sub> # build/edit a manifest (see below)
```

Fanning a fetcher out across the programs in a Paramify workspace is its own
step, because the API takes project UUIDs while people know their programs by
name. `paramify programs` closes that gap — list what's there, pick by name, and
it writes the targets for you:

```bash
paramify programs list # name + UUID for every program
paramify programs target # choose interactively, then wire them up
paramify programs target --all \
--cert-uri https://example.gov/cpo --report-from 2026-01-01
```

With no fetcher argument it targets every manifest entry that takes a program, so
one command fans all of them out at once. Re-running it tops the manifest up
rather than duplicating targets.

A target carries only what varies per program — `project_id` and its readable
`program_name`. Everything shared is written once to `platforms.paramify.config`:
the Certification Package Overview URI (not in Paramify's API, one value per
workspace) and the report period start. Every interactive run shows both with
whatever the manifest holds today as the prompt default — enter keeps it and
writes nothing, typing over it updates it — so `paramify programs target` is
equally how you add a program and how you roll the report window forward.
Passing `--cert-uri`/`--report-from` overwrites what's there without asking.

`--report-from` is checked for an ISO date up front — an unparseable one produces
an empty report window, which drops every closed issue from the report without
failing.

Both subcommands need `PARAMIFY_API_TOKEN` with read scope and accept `--json`
(under `--json` nothing prompts, so pass the flags).

> Back-compat: `python -m framework.runner <cmd>` and `python -m framework.tui`
> still work and are exactly equivalent to the corresponding `paramify`
> subcommands.
Expand Down
8 changes: 8 additions & 0 deletions docs/fetcher_contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,9 +110,17 @@ paramify run <manifest.yaml> # collect: enveloped JSON + _run_metadata.js
paramify runs # past runs under the output dir (newest first)
paramify evidence <file> # read one evidence file (normalizing the envelope)
paramify upload [run-dir] # push one run's evidence to Paramify (default: latest run)
paramify programs list # programs in the Paramify workspace: readable name + project UUID
paramify programs target [fetcher ...] # select programs by name and write them as fanout targets
paramify manifest <sub> # build/edit a manifest file (init/new/add/remove/set-config/set-secret/add-target/remove-target/...)
```

`paramify programs` is the only command that reads live workspace state (`GET
/projects`, needs `PARAMIFY_API_TOKEN`); it exists because the API identifies
programs by UUID while operators know them by name. `target` composes
`add_target` under the hood, so it produces exactly the manifest a hand-written
`manifest add-target` would.

Every `manifest` subcommand also accepts `--json`, emitting a stable `{ok, path, errors}` object so an agent can build a manifest step by step and read `errors` to see what's still missing.

`list`/`validate` fail with a non-zero exit if any `fetcher.yaml` is schema-invalid. The envelope the runner produces is validated against `envelope_schema.json`, but a fetcher's *runtime* behavior (exit codes, output paths, etc.) is not yet automatically verified — that arrives with integration tests.
Expand Down
41 changes: 41 additions & 0 deletions docs/run_manifest_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,6 +241,47 @@ the AI CLI; without it you get the human-readable rendering. Mutating commands
return `{ok, path, errors}` under `--json` so a caller can confirm the write
landed and surface any validation messages.

### Targets from a Paramify workspace

For fetchers whose target is a Paramify program, the targets can be filled in
from the workspace instead of by hand — the API needs project UUIDs, which nobody
wants to copy:

```bash
paramify programs list # readable name + UUID
paramify programs target # pick interactively, write targets
paramify programs target --all \ # every program
--cert-uri https://… --report-from 2026-01-01 # non-interactive / --json
```

With no fetcher argument it targets every manifest entry whose `target_schema`
declares `project_id`. Programs already targeted are skipped, so re-running tops
the manifest up instead of duplicating entries.

A target gets only what varies per program: `project_id` and `program_name` (the
readable label — the fetcher uses it for its evidence filename, the uploader for
the artifact title).

Anything the targeted fetchers need that *doesn't* vary per program is written to
`platforms.<category>.config`: `--cert-uri` (the Certification Package Overview
URI) and `--report-from` (the report period start). Each is shown on every
interactive run, with the value in force as the prompt default and a line saying
where it comes from (`platforms.paramify`, an entry's own config, or not set
yet); enter keeps it and leaves the manifest alone, typing over it updates the
category value. Entries that resolve to *different* values are reported as such
and no default is offered, since either one shown as the answer would misreport
the other. Supplying the flag skips the prompt and overwrites an existing value.
`--report-from` is validated as an ISO date before it's written.

Needs `PARAMIFY_API_TOKEN` with read scope; under `--json` nothing prompts, so
pass `--program`/`--all` plus whichever shared values are still missing.

The same split applies generally: values that vary per fanout iteration belong in
`targets[]`, and values shared across a category belong under `platforms.<category>.config`
— which the runner merges as *platform defaults ← platform values ← per-fetcher
values*, so a manifest can set **any** field a fetcher declares once at the
platform level, even one declared in the fetcher's own `config_schema`.

### Build / edit a manifest

The `manifest` subcommands read each fetcher's `fetcher.yaml` and write the
Expand Down
49 changes: 49 additions & 0 deletions fetchers/_categories/paramify.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Category-level metadata for the Paramify FedRAMP report fetchers.
#
# Unlike most categories (which pull evidence FROM a third-party system INTO
# Paramify), these fetchers read FROM Paramify's own REST API and produce the
# FedRAMP Consolidated Rules 2026 vulnerability-reporting artifacts
# (VER-RPT-AVI, VER-RPT-VDT, VER-TFR-MRH).
#
# Access: Paramify REST API v0 with a Bearer token that has read scope on the
# target project's issues and deviations.

name: paramify
description: >-
FedRAMP 20x vulnerability-reporting artifacts generated from Paramify issue
data (accepted vulnerabilities, vulnerability detail report, and the
historical VER-activity snapshot).

# Config shared by every paramify fetcher. The runner injects these env vars for
# any fetcher with category: paramify. Set values once per run under manifest
# platforms.paramify.config.
config_schema:
cert_package_uri:
type: string
required: true
env: PARAMIFY_CERT_PACKAGE_URI
description: >-
Public Certification Package Overview URI, written into every VER report as
certificationPackageOverviewUri. One workspace publishes one such URI, so it
lives here rather than per fetcher or per program — set it once and all
three reports, across every program, carry it.
api_base_url:
type: string
default: https://app.paramify.com/api/v0
env: PARAMIFY_API_BASE_URL
description: Paramify REST API base URL. Point at a non-production instance for testing.
http_timeout:
type: integer
default: 300
env: PARAMIFY_HTTP_TIMEOUT
description: >-
Per-request timeout in seconds. The unfiltered /issues call is large
(~1.9 MB / ~75-120 s on a ~2k-issue project), hence the high default.

auth:
# The fetchers read the token from PARAMIFY_API_TOKEN (falling back to
# PARAMIFY_UPLOAD_API_TOKEN). Base URL defaults to app.paramify.com and can
# be overridden per environment.
description: >-
Paramify REST API Bearer token with read scope on the project's issues and
deviations. See fetchers/paramify/README.md.
Loading
Loading