Skip to main content

A markdown hub between security scanners and Ghostwriter + BookStack.

Project description

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

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>/
    evidence/
methodology/
  library/<book>/
  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/…/evidence/ Ghostwriter evidence images attached to a finding
methodology/library/ BookStack books/pages markdown-native, mirrors verbatim
methodology/checklists/ nothing per-engagement working copies, cp -r from library
.grison/ creds + sync state; auto-gitignored, never commit

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 lists. Rejected inside a field: tables, ordered lists, images, headings (the ## section headers are grison structure mapping to Ghostwriter's separate fields, not field content). BookStack pages are markdown-native and mirror verbatim, no conversion.

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

License

MIT. Named after the mustelid.

Project details


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.1.2.tar.gz (132.3 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.1.2-py3-none-any.whl (98.7 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: grison-0.1.2.tar.gz
  • Upload date:
  • Size: 132.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","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.1.2.tar.gz
Algorithm Hash digest
SHA256 49ef15fb72303ed297ac05f3159f02adaf0e3b7ed491b56c478973c1dd06e1f3
MD5 f315f4c3c8f61f541f4360f563dd82ca
BLAKE2b-256 2f59846a43efd67a961d693dc072dc4037b0cc078754eb82de0e7b4f891fd30d

See more details on using hashes here.

File details

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

File metadata

  • Download URL: grison-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 98.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: uv/0.11.28 {"installer":{"name":"uv","version":"0.11.28","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.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 f75f3effdf67e2f505e2ae278356a0838b8c932e053e6fecc64a66327971d7fe
MD5 06586e0943f27e005f1a17f271c57248
BLAKE2b-256 5fccaea95cf008663da30bf06600abb4f81bd53c0f5ec0686ef68c3153454f0b

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page