Skip to main content

A markdown hub between security scanners and Ghostwriter + BookStack.

Project description

grison

grison is a markdown hub between security scanners and Ghostwriter + BookStack. It parses scanner exports into a single house schema of markdown findings, and reconciles a local git-like workspace with Ghostwriter (findings, "GW") and BookStack (methodology) via a 3-way sync. Markdown is the lingua franca — which also makes an LLM editing the workspace the transform layer, so grison itself has no AI subsystem; its job is to validate and sync those edits safely.

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

Install

pipx install grison       # or: uv tool install grison
grison --help

For development:

uv sync                   # Python 3.11+, managed with uv
uv run pytest

The three verbs

Command Does
grison parse <path…> scanner export(s) → markdown findings in findings/inbox/ (offline; auto-detects the scanner)
grison status <path…> per-record validity: schema / enums / CVSS / CWE / the GW HTML whitelist
grison sync reconcile the workspace with Ghostwriter + BookStack; direction derived per record

There is no pull/push (sync derives direction), no init (the first sync bootstraps the workspace + a .grison/env creds template), and no validate (status reports it, sync enforces it). Moving a finding between cells is a plain cp/mv.

Data model — a 2×2, mirrored faithfully

The workspace tree is the model. Segment 1 = domain (backend), segment 2 = tier:

findings/                 # ⇄ Ghostwriter
  inbox/                  # parse output — local-only, triage then cp into a report
  library/                # ⇄ GW finding table
  reports/<id>-<slug>/    # ⇄ GW reportedFinding — one dir per EXISTING report
    <rfid>-<slug>.md
    evidence/*.png        # images attached to a finding
methodology/              # ⇄ BookStack
  library/<book>/<page>.md
  checklists/<engagement>/  # per-engagement copies — local-only, cp -r from library/
.grison/                  # creds + state; always gitignored

Location is identity. A file's directory fixes its remote target; sync matches by remote id, not filename (filenames are cosmetic). A file whose location disagrees with its stored id is a move → a new record — which is all the old instantiate/promote verbs did. grison syncs findings + evidence into reports it never creates.

Finding markdown schema (by example)

One tier-agnostic schema; structured facts in 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 the embedded CWE index
affected_entities: |        # instances only
  https://app.example/
evidence:                   # instances only; images attached to this finding
  - { 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; enums carry their Ghostwriter ids (derived, never stored). CVSS accepts well-formed 3.0 or 3.1 as-authored.

Converter whitelist (markdown ⇄ Ghostwriter HTML)

GW's rich-text fields use a tiny closed vocabulary, and the converter (grison/markdown/) fails loudly on anything outside it rather than corrupt silently:

  • Inline: **bold**<strong>, `code`<code>, *em*<em>, [t](u)<a>.
  • Block: paragraphs ⇄ <p>, - lists ⇄ <ul><li> (never <ol>).
  • Rejected in a field: tables, ordered lists, images, headings. The ## section headers are grison structure that map to GW's separate fields, not field content.

(BookStack methodology pages are markdown-native, so they mirror verbatim — no converter.)

Sync + guardrails

Direction isn't chosen — the 3-way base (synced.hash) determines it per record: only-local-changed → push, only-remote → pull, both → collision (the remote side is written to an x.remote.md sidecar; local is never overwritten — resolve then sync --force-local/--force-remote <file>), converged-under-a-stale-base → repair.

Guardrails stop anomalous or destructive outcomes, never routine ones (no confirmation nagging; --dry-run is opt-in). Three layers: validation (silent when green), trip-wires that fire only on anomaly (mass-change guard, structure drift, collision, duplicate identity, broken link), and a pre-write snapshot of every remote write batch (with a paired rollback.py) so every write is reversible.

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.1.tar.gz (131.4 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.1-py3-none-any.whl (97.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: grison-0.1.1.tar.gz
  • Upload date:
  • Size: 131.4 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.1.tar.gz
Algorithm Hash digest
SHA256 80acbc18a59e2e07ecd92cc95a2b168a9a9b9d64629c770185da4bd706b81d43
MD5 b6aec848ddd1544981f3c92316c0491b
BLAKE2b-256 a15e601bd0f02cd1b6f815cc647ca5f3d7a084d880ae534b5352bd493f1c973e

See more details on using hashes here.

File details

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

File metadata

  • Download URL: grison-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 97.9 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.1-py3-none-any.whl
Algorithm Hash digest
SHA256 43ce4fd706141d55b2464850daa01562d779ff0d05778d01105ffd670c99de1e
MD5 22d8cee59bf5da90f5c9b5c8950d83fa
BLAKE2b-256 c3f0908c4c348cd2111385c9cca931b01464952cefe5a7cc95332b15e20e9ba8

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