Skip to main content

grison

PyPI Python License: MIT

grison turns raw security-scanner exports into clean markdown findings, gives you a plain-files workspace to triage and edit them in, and syncs the result into Ghostwriter (findings + evidence) and BookStack (methodology) with a git-style 3-way merge.

Markdown in the middle is the point: findings are files, so the transform layer is whatever edits files best — a human in any editor, a script, or an LLM let loose on the workspace. grison itself has no AI subsystem; its job is to validate what changed and sync it safely.

Supported scanner exports (auto-detected): Acunetix, Burp, Nessus, Nmap, OpenVAS, Qualys, sslyze, ZAP.

Install

pipx install grison            # or: uv tool install grison
grison --install-completion    # optional: shell tab completion (once, then restart shell)

For development: uv sync && uv run pytest (Python 3.11+, managed with uv).

Quickstart

mkdir engagement && cd engagement
grison parse scan.nessus burp.xml    # first run scaffolds the workspace
# triage findings/inbox/*.md, edit freely, move the keepers into place:
cp findings/inbox/sqli-login.md findings/reports/6-acme-q3/
grison status findings/              # validity report per record
grison sync                          # reconcile with Ghostwriter + BookStack

The three verbs

Command Does
grison parse <path…> scanner export(s) → markdown findings in findings/inbox/. Offline. Auto-detects the scanner (--scanner to force), --min-severity high,critical or medium-critical to filter, --dry-run to preview.
grison status <path…> per-record validity: schema, enums, CVSS vector↔score, CWE ids, Ghostwriter HTML whitelist. Offline.
grison sync reconcile the workspace with Ghostwriter (findings) and BookStack (methodology). Direction is derived per record — see below. --dry-run previews the plan.

There is deliberately no pull/push (sync derives direction), no init (the first run of any verb scaffolds the workspace), and no validate (status reports, sync enforces). Moving a finding between tiers is a plain cp/mv.

The workspace

The directory tree is the data model — location is identity:

findings/
  inbox/
  library/
  reports/<id>-<slug>/
    narrative/
    evidence/
methodology/
  library/<book>/<chapter>/
  checklists/<engagement>/
.grison/
Path Mirrors Notes
findings/inbox/ nothing parse output; triage here, then cp into a tier
findings/library/ Ghostwriter finding library reusable templates
findings/reports/<id>-<slug>/ Ghostwriter reported findings one dir per existing report; grison never creates reports
findings/reports/…/narrative/ Ghostwriter report extraFields one editable markdown file per report section (exec summary, methodology, …); 3-way per section. .report.yml holds the read-only metadata mirror
findings/reports/…/evidence/ Ghostwriter evidence images attached to a finding
methodology/library/ BookStack books/chapters/pages markdown-native, mirrors verbatim
methodology/checklists/ nothing per-engagement working copies, cp -r from library
.grison/ creds + sync state; auto-gitignored, never commit

BookStack's structure mirrors losslessly: library/<book>/<page>.md for pages at a book's root, library/<book>/<chapter>/<page>.md for chaptered pages. Every book and chapter — including empty ones — materializes as a directory holding a .book.yml / .chapter.yml mirror (ids, name, description, shelf membership, chapter order; pull-only). Page sort order (priority) and page tags live in the page frontmatter and sync in both directions. Moving a file between chapter directories moves the page on BookStack; a page moved into a chapter remotely relocates the local file on the next sync.

Sync matches records by remote id stored in the file, not by filename (filenames are cosmetic). A file whose directory disagrees with its stored id is a move and becomes a new record at the destination.

Finding schema

One tier-agnostic schema: structured facts in YAML frontmatter, prose in fixed ## sections.

---
grison:
  kind: finding
  tier: instance            # library | instance
  gw: { table: reportedFinding, id: 183, report_id: 6 }
  synced: { hash: sha256:…, at: 2026-07-14T12:00:00Z }  # the 3-way merge base
severity: high              # informational|low|medium|high|critical
finding_type: web           # network|physical|wireless|web|mobile|cloud|host
cvss: { vector: "CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H", score: 9.8 }
cwe: ["CWE-79"]             # validated against an embedded CWE index
affected_entities: |        # instances only
  https://app.example/
evidence:                   # instances only
  - { file: evidence/shell.png, caption: Shell, friendly_name: shell, gw: { id: 38, hash: sha256:… } }
---

# {title}
## Description
## Impact
## Mitigation
## Replication Steps
## References

The pydantic models in grison/model/ are the schema. CVSS is accepted as well-formed 3.0 or 3.1, as authored. CWE ids are validated against a vendored index of the MITRE catalog, so everything works fully offline.

How sync decides direction

Nobody chooses push or pull — the stored merge base (synced.hash) determines it per record:

  • only local changed → push
  • only remote changed → pull
  • both changed → collision: the remote version is written to an x.remote.md sidecar, local is never overwritten; resolve, then grison sync --force-local <file> or --force-remote <file>
  • both converged under a stale base → repair the base, write nothing

Guardrails

Guardrails stop anomalous or destructive outcomes, never routine ones — no confirmation prompts, --dry-run is opt-in. Three layers:

  1. Validation — silent when green; a record must be valid to sync.
  2. Trip-wires — fire only on anomaly: mass-change guard, structure drift, collision, duplicate identity, broken evidence link.
  3. Snapshots — every remote write batch is snapshotted first, with a paired rollback.py, so every write is reversible.

Markdown ⇄ Ghostwriter HTML

Ghostwriter's rich-text fields use a small closed vocabulary, and the converter fails loudly on anything outside it rather than corrupt silently. Inline: **bold**, `code`, *em*, [links](…). Block: paragraphs and unordered/ordered lists (- and 1. , one level of 2-space sub-items, freely nested either way). Rejected inside a field: tables, images, headings (the ## section headers are grison structure mapping to Ghostwriter's separate fields, not field content). Constructs grison canonicalizes on purpose — editor highlight spans, non-standard link rel/target — are reported as sync warnings when dropped, never absorbed silently. BookStack pages are markdown-native and mirror verbatim, no conversion; pages authored in the WYSIWYG editor are skipped loudly until converted to markdown in BookStack.

What syncs, what doesn't

Every remote field has an explicit verdict — "not synced" always means grison can neither lose it nor damage it (partial updates never touch unlisted columns).

Two-way synced (in the merge base — edits on either side propagate, both-changed collides): finding title, severity, type, CVSS vector + score, all five prose sections, affected entities; tags and CWE ids (Ghostwriter's tag table, CWE:<n> convention); evidence images with caption / friendly name / description (per-image merge base); report narrative sections (report.extraFields), each section its own merge base; BookStack page body, name, priority, tags, and chapter/book location.

Mirrored read-only (pull-only; local edits are detected and rejected, never silently discarded): report metadata (.report.yml — project, client, dates, lifecycle flags); BookStack book/chapter/shelf structure (.book.yml, .chapter.yml, .shelves/*.yml — carry a READ-ONLY header; hand-edits error with a .remote.yml sidecar).

Deliberately not synced (remote-owned, unreachable by grison's writes): Ghostwriter report-finding position (per-severity kanban order, server renumbers), complete/assignedTo QA state, finding/report comments, Observations, report templates and reportConfiguration, report-level evidence rows (never deleted by grison), dead-in-corpus columns (hostDetectionTechniques, networkDetectionTechniques, findingGuidance, per-finding extraFields); BookStack container tags/covers/templates, page template flag, revision history. Renaming books/chapters happens in BookStack, not by renaming local dirs (a rename tripwire blocks the ambiguous case). Severity/finding-type id mappings are verified against the live instance at sync start and abort on drift.

Configuration

The first run in a directory scaffolds .grison/env — a commented template, chmod 600, auto-gitignored. Paste values there, or set the same keys as environment variables (env vars override the file; useful for CI):

Key For
GRISON_GW_URL, GRISON_GW_TOKEN Ghostwriter — GraphQL API token
GRISON_BS_URL, GRISON_BS_TOKEN_ID, GRISON_BS_TOKEN_SECRET BookStack — REST API token; only needed for methodology sync
GRISON_CF_CLIENT_ID, GRISON_CF_CLIENT_SECRET optional — Cloudflare Access service token, if your deployment sits behind CF Access

Settings

Two more keys in the same file — non-secret behavior toggles, same env-var-overrides-file precedence:

Key Values Default Does
GRISON_GIT commit off drive git around sync/parse: commit a checkpoint of any dirty tree before the run, then commit the result after (grison: sync (...), grison: parse <scanner>) — only when the workspace root already sits inside a git repo. grison never inits, pushes, branches, or touches a remote; --dry-run commits nothing; any git failure is a warning, never a command failure.
GRISON_CLAUDE_MD off on scaffold a CLAUDE.md operator-notes file on first bootstrap (never overwritten if one already exists)

License

MIT. Named after the mustelid.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

grison-0.3.2.tar.gz (229.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

grison-0.3.2-py3-none-any.whl (154.3 kB view details)

Uploaded Python 3

File details

Details for the file grison-0.3.2.tar.gz.

File metadata

  • Download URL: grison-0.3.2.tar.gz
  • Upload date:
  • Size: 229.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for grison-0.3.2.tar.gz
Algorithm Hash digest
SHA256 253047f91764defedcee3392998fde4569a364dbe73b65814ba9b50be4965ac6
MD5 c19462f2a939b4a5e6d08c48ceb0aa75
BLAKE2b-256 140a9831b125de22a1af47b02aff11417cdc76eb4f025f2f7470ab4e1d68ec44

See more details on using hashes here.

File details

Details for the file grison-0.3.2-py3-none-any.whl.

File metadata

  • Download URL: grison-0.3.2-py3-none-any.whl
  • Upload date:
  • Size: 154.3 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for grison-0.3.2-py3-none-any.whl
Algorithm Hash digest
SHA256 9592dd727d0b26009f0016c5b48f1f00a81e4771029ff7783f26f97c7b1e33bc
MD5 6517301e3ee9d75e1233b3503202f87d
BLAKE2b-256 27a95e2250d57b5e96db673ebf9ee35c4d77d31ba095ff44ad560375a199b3c2

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.3.2 This release

2 files

0.3.1

2 files

0.3.0

2 files

0.2.1

2 files

0.2.0

2 files

0.1.2

2 files

0.1.1

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page