Skip to main content

A markdown hub between security scanners and Valiente's Ghostwriter + BookStack.

Project description

grison

grison is a markdown hub between security scanners and Valiente's infra. It parses scanner exports into a single house schema of markdown findings, and reconciles a local git-like workspace with Ghostwriter (findings) 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.

Install

uv sync           # Python 3.11+, managed with uv
uv run grison --help

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.0.tar.gz (131.0 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.0-py3-none-any.whl (97.8 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: grison-0.1.0.tar.gz
  • Upload date:
  • Size: 131.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","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":null}

File hashes

Hashes for grison-0.1.0.tar.gz
Algorithm Hash digest
SHA256 94be48ba6c3300effcd5d52d965eadf3ca90e02781acf5a0bff1217c048178da
MD5 ea8d19915eae21c33f42ef287d6801c0
BLAKE2b-256 594614c7953a53fd61ad15aa7bfedacfe0ffac44a43846d64c5b1cbc7d20ceaa

See more details on using hashes here.

File details

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

File metadata

  • Download URL: grison-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 97.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.7 {"installer":{"name":"uv","version":"0.11.7","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":null}

File hashes

Hashes for grison-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 032974c63e251138230e5536fd32fe9f74b46a5dbd6b457cbfb1c719ffecbd4f
MD5 51f87ecf53df61baaa1c40ef015e2ad7
BLAKE2b-256 6fe8088438a44edcb97c2bbf964302792d4aafd07a4bd3c73e2faf466cddabe5

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