From 75cd48c6b69ad2787d85cf3c36cc1f23a8a786cf Mon Sep 17 00:00:00 2001 From: stacknil Date: Sun, 5 Jul 2026 18:00:21 +0800 Subject: [PATCH] feat(patterns): add maturity and provenance gates --- .github/workflows/markdown-lint.yml | 7 + .pre-commit-config.yaml | 10 + README.md | 36 ++- docs/reviewer-brief.md | 36 ++- notes/80-blue-team/ai-forensics-public.md | 6 + notes/80-blue-team/containment-public.md | 6 + .../understanding-ai-supply-chains-public.md | 6 + patterns/README.md | 77 ++++-- patterns/ai/human-verification-boundary.md | 68 +++++ patterns/auth/authentication-burst-triage.md | 67 +++++ patterns/auth/multi-user-probing.md | 13 +- patterns/auth/ssh-bruteforce-triage.md | 13 +- patterns/auth/sudo-burst-review.md | 13 +- .../cloud/iam-policy-attachment-review.md | 13 +- patterns/detection/alert-deduplication.md | 13 +- patterns/detection/bounded-correlation.md | 13 +- patterns/detection/cooldown-suppression.md | 13 +- patterns/parsing/parser-uncertainty.md | 67 +++++ .../repository-baseline-governance.md | 68 +++++ .../sbom-dependency-diff-review.md | 13 +- patterns/supply-chain/sbom-policy-warning.md | 66 +++++ schemas/pattern-library.json | 54 ++++ scripts/check_pattern_library.py | 249 ++++++++++++++++++ scripts/render_readme_snapshot.py | 12 +- scripts/run_markdownlint.py | 13 +- 25 files changed, 866 insertions(+), 86 deletions(-) create mode 100644 patterns/ai/human-verification-boundary.md create mode 100644 patterns/auth/authentication-burst-triage.md create mode 100644 patterns/parsing/parser-uncertainty.md create mode 100644 patterns/repository/repository-baseline-governance.md create mode 100644 patterns/supply-chain/sbom-policy-warning.md create mode 100644 schemas/pattern-library.json create mode 100644 scripts/check_pattern_library.py diff --git a/.github/workflows/markdown-lint.yml b/.github/workflows/markdown-lint.yml index b27bfe6..01c6f00 100644 --- a/.github/workflows/markdown-lint.yml +++ b/.github/workflows/markdown-lint.yml @@ -5,12 +5,14 @@ name: Markdown Validation push: paths: - "README.md" + - "docs/**/*.md" - "patterns/**/*.md" - "TryHackMe/**/*.md" - "TryHackMe/_meta/TAGS.md" - "notes/**/*.md" - "schemas/**/*.json" - "scripts/check_markdown.py" + - "scripts/check_pattern_library.py" - "scripts/render_readme_snapshot.py" - "scripts/render_tags_doc.py" - "scripts/generate_markdownlint_debt.py" @@ -24,12 +26,14 @@ name: Markdown Validation pull_request: paths: - "README.md" + - "docs/**/*.md" - "patterns/**/*.md" - "TryHackMe/**/*.md" - "TryHackMe/_meta/TAGS.md" - "notes/**/*.md" - "schemas/**/*.json" - "scripts/check_markdown.py" + - "scripts/check_pattern_library.py" - "scripts/render_readme_snapshot.py" - "scripts/render_tags_doc.py" - "scripts/generate_markdownlint_debt.py" @@ -67,6 +71,9 @@ jobs: - name: Verify README.md snapshot is in sync with tracked notes run: python scripts/render_readme_snapshot.py --check + - name: Validate pattern library contract + run: python scripts/check_pattern_library.py + - name: Run Markdown checks run: python scripts/check_markdown.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9eaeaf6..c0ecbbc 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,6 +17,16 @@ repos: files: >- ^(README\.md|TryHackMe/(?!_meta/).+\.md|notes/.+\.md| scripts/render_readme_snapshot\.py)$ + - id: validate-pattern-library + name: validate-pattern-library + entry: python scripts/check_pattern_library.py + language: python + additional_dependencies: + - PyYAML>=6.0,<7.0 + pass_filenames: false + files: >- + ^(README\.md|docs/.+\.md|patterns/.+\.md|schemas/pattern-library\.json| + scripts/check_pattern_library\.py|notes/.+\.md)$ - id: validate-frontmatter name: validate-frontmatter entry: python scripts/check_markdown.py diff --git a/README.md b/README.md index a9f950d..b7bcce7 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,32 @@ Public, sanitized security write-ups from authorized labs and training platforms > A local-first public knowledge base focused on methodology, reasoning, and reusable security patterns, not copy-paste exploitation. +## Featured Security Patterns + +Current extraction metric: **8 stable reusable security patterns extracted +from 10 distinct supporting notes**. + +| Pattern | Review focus | +| --- | --- | +| [Parser uncertainty](patterns/parsing/parser-uncertainty.md) | make unsupported input visible instead of treating it as negative evidence | +| [Authentication burst triage](patterns/auth/authentication-burst-triage.md) | bound failure clusters without claiming compromise | +| [Multi-user probing](patterns/auth/multi-user-probing.md) | distinguish username spread from ordinary repeated failure | +| [Alert deduplication](patterns/detection/alert-deduplication.md) | collapse duplicate alerts without erasing evidence | +| [Bounded correlation](patterns/detection/bounded-correlation.md) | require a reproducible join key, sequence, and window | +| [Repository baseline governance](patterns/repository/repository-baseline-governance.md) | review suppression drift as security-adjacent code | +| [SBOM policy warning](patterns/supply-chain/sbom-policy-warning.md) | explain a warning without inventing a package-safety verdict | +| [Human verification boundary](patterns/ai/human-verification-boundary.md) | keep AI assistance separate from the accountable finding | + +The [Pattern Library](patterns/README.md) is the primary reusable output. The +larger note corpus remains its source archive and evidence base. + ## At A Glance -Current public snapshot: +Supporting source archive: -- `157` active public notes -- `100` active TryHackMe notes -- `57` topic-organized notes under `notes/` +- `157` public notes retained as source material +- `100` TryHackMe source notes +- `57` topic-organized source notes under `notes/` - `9` organized TryHackMe learning tracks - public governance for taxonomy, placeholders, publication, and maintenance @@ -38,7 +57,7 @@ The emphasis is on: ## What You Will Find -The live public corpus is currently centered on **TryHackMe** and supplemented by a smaller topic-organized `notes/` tree. +The supporting source corpus is centered on **TryHackMe** and supplemented by a topic-organized `notes/` tree. | Track | Notes | Focus | | --- | --- | --- | @@ -52,7 +71,7 @@ The live public corpus is currently centered on **TryHackMe** and supplemented b | [TryHackMe/80-blue-team](TryHackMe/80-blue-team) | `6` | SIEM, detection, analyst notes | | [TryHackMe/90-events](TryHackMe/90-events) | `32` | event and challenge write-ups, sanitized for publication | -Additional organized notes outside the TryHackMe tree: +Additional source notes outside the TryHackMe tree: - `notes/` currently contains `57` active public notes @@ -77,9 +96,12 @@ If you are browsing this repo for the first time: ```text sec-writeups-public/ ├── patterns/ +│ ├── ai/ │ ├── auth/ -│ ├── detection/ │ ├── cloud/ +│ ├── detection/ +│ ├── parsing/ +│ ├── repository/ │ └── supply-chain/ ├── TryHackMe/ │ ├── 00-foundations/ diff --git a/docs/reviewer-brief.md b/docs/reviewer-brief.md index b1f4f7d..f0ba56e 100644 --- a/docs/reviewer-brief.md +++ b/docs/reviewer-brief.md @@ -6,19 +6,21 @@ Security notes are often hard to publish safely: they either leak too much, coll ## What it does -`sec-writeups-public` is a public, sanitized security note repository with: +`sec-writeups-public` is a public, sanitized pattern library backed by a +security source-note repository with: -- a large TryHackMe-centered note corpus -- topic-organized notes outside the training-platform tree +- a stable pattern library extracted from supporting source notes +- evidence-bounded links from patterns to related implementations +- a larger TryHackMe-centered source-note corpus - taxonomy, placeholder, and publication governance - local checks that keep the public corpus maintainable ## Reviewer Evidence -- Reproducible command: `python scripts/render_readme_snapshot.py --check` -- Deterministic outputs: rendered README snapshots, generated tag docs, placeholder audit output, taxonomy docs, and markdownlint debt reports. -- Tests / CI: local publication checks, placeholder checks, markdown checks, pre-commit hooks, and GitHub Actions maintenance workflows. -- Release evidence: public corpus snapshot, governance docs, sanitization checklist, publication workflow, and maintenance checkpoints. +- Reproducible command: `python scripts/check_pattern_library.py` +- Deterministic outputs: pattern maturity, provenance counts, project links, case-study backlinks, rendered README snapshots, and generated tag docs. +- Tests / CI: pattern-contract validation, publication checks, placeholder checks, markdown checks, pre-commit hooks, and GitHub Actions workflows. +- Release evidence: stable pattern index, source-note links, governance docs, sanitization checklist, and maintenance checkpoints. - Non-goals: raw exploit logs, private evidence dumps, live target identifiers, weaponized exploit chains, or unsanitized challenge transcripts. ## Quick run @@ -26,6 +28,7 @@ Security notes are often hard to publish safely: they either leak too much, coll ```bash python scripts/render_tags_doc.py --check python scripts/render_readme_snapshot.py --check +python scripts/check_pattern_library.py python scripts/check_placeholders.py notes/80-blue-team/soc-fundamentals.md python scripts/check_markdown.py python -m pre_commit run --files notes/80-blue-team/soc-fundamentals.md @@ -33,19 +36,23 @@ python -m pre_commit run --files notes/80-blue-team/soc-fundamentals.md ## Sample output -The current public snapshot in the README reports: +The current reviewer-facing metric is: -- `111` active public notes -- `100` active TryHackMe notes -- `11` topic-organized notes under `notes/` +- `8` stable reusable security patterns +- `10` distinct supporting notes behind those stable patterns +- `13` total cards across stable and reviewed maturity levels -A representative public-safe note is `notes/80-blue-team/soc-fundamentals.md`, which uses summary-first structure, reusable concepts, and placeholders instead of live identifiers. +A representative stable card is +`patterns/detection/bounded-correlation.md`, which makes the signal, +false-positive contexts, evidence limits, implementation bridge, and source +notes independently reviewable. ## What this proves - security writing can be structured like an engineered artifact - publication safety can be enforced with explicit governance -- public notes can stay reusable and maintainable instead of one-off dumps +- source notes can be distilled into stable, reusable security patterns +- pattern maturity and evidence links can be enforced in CI - reviewer-facing documentation discipline extends beyond code repos ## Safety / boundaries @@ -63,4 +70,5 @@ A representative public-safe note is `notes/80-blue-team/soc-fundamentals.md`, w ## Next milestone -Keep deepening blue-team and detection-oriented notes while improving reviewer-facing indexes for the strongest public writing paths. +Promote reviewed cards only when new evidence or implementation work increases +their decision value; avoid growing the source archive as an end in itself. diff --git a/notes/80-blue-team/ai-forensics-public.md b/notes/80-blue-team/ai-forensics-public.md index 112f5ac..7e5a7b6 100644 --- a/notes/80-blue-team/ai-forensics-public.md +++ b/notes/80-blue-team/ai-forensics-public.md @@ -341,3 +341,9 @@ Convenience can collide with privacy law, chain of custody, and admissibility. * NIST AI Risk Management Framework * legal references on the Daubert standard and expert testimony * privacy-preserving federated learning references + +--- + +## 11. Related Patterns + +* [Human Verification Boundary](../../patterns/ai/human-verification-boundary.md) diff --git a/notes/80-blue-team/containment-public.md b/notes/80-blue-team/containment-public.md index b8856d4..0688953 100644 --- a/notes/80-blue-team/containment-public.md +++ b/notes/80-blue-team/containment-public.md @@ -471,3 +471,9 @@ environments. * Prompt injection defensive guidance * Human-in-the-loop incident response patterns * Ransomware triage and evidence-validation playbooks + +--- + +## 16. Related Patterns + +* [Human Verification Boundary](../../patterns/ai/human-verification-boundary.md) diff --git a/notes/80-blue-team/understanding-ai-supply-chains-public.md b/notes/80-blue-team/understanding-ai-supply-chains-public.md index 693dcb3..81dc72b 100644 --- a/notes/80-blue-team/understanding-ai-supply-chains-public.md +++ b/notes/80-blue-team/understanding-ai-supply-chains-public.md @@ -615,3 +615,9 @@ reproducibility, not page aesthetics. * SafeTensors documentation * PyPI analysis of the Ultralytics supply-chain attack * PyTorch advisory on the torchtriton dependency confusion incident + +--- + +## 16. Related Patterns + +* [SBOM Policy Warning](../../patterns/supply-chain/sbom-policy-warning.md) diff --git a/patterns/README.md b/patterns/README.md index 3b68635..f559c74 100644 --- a/patterns/README.md +++ b/patterns/README.md @@ -1,49 +1,82 @@ # Pattern Library -Pattern Library v0.1 is the stable entry point for reusable security detection, -triage, and review patterns. Read these cards before browsing the larger note +This is the stable entry point for reusable security detection, triage, and +review patterns. Read these cards before browsing the larger source-note archive when you need a compact, evidence-bounded defensive workflow. -## Authentication +Current extraction metric: **8 stable reusable security patterns extracted +from 10 distinct supporting notes**. -* [SSH brute-force triage](auth/ssh-bruteforce-triage.md) -* [Multi-user probing](auth/multi-user-probing.md) -* [Sudo burst review](auth/sudo-burst-review.md) +## Featured Stable Patterns -## Detection Engineering +| Pattern | Primary implementation | +| --- | --- | +| [Parser uncertainty](parsing/parser-uncertainty.md) | LogLens | +| [Authentication burst triage](auth/authentication-burst-triage.md) | LogLens | +| [Multi-user probing](auth/multi-user-probing.md) | LogLens | +| [Alert deduplication](detection/alert-deduplication.md) | telemetry-lab | +| [Bounded correlation](detection/bounded-correlation.md) | telemetry-lab | +| [Repository baseline governance](repository/repository-baseline-governance.md) | repo-sentinel-lite | +| [SBOM policy warning](supply-chain/sbom-policy-warning.md) | sbom-diff-and-risk | +| [Human verification boundary](ai/human-verification-boundary.md) | telemetry-lab | -* [Alert deduplication](detection/alert-deduplication.md) -* [Cooldown suppression](detection/cooldown-suppression.md) -* [Bounded correlation](detection/bounded-correlation.md) +## Reviewed Patterns -## Cloud +These cards remain useful but are not part of the featured stable set: +* [SSH brute-force triage](auth/ssh-bruteforce-triage.md) +* [Sudo burst review](auth/sudo-burst-review.md) +* [Cooldown suppression](detection/cooldown-suppression.md) * [IAM policy attachment review](cloud/iam-policy-attachment-review.md) +* [SBOM dependency diff review](supply-chain/sbom-dependency-diff-review.md) -## Supply Chain +## Maturity Model -* [SBOM dependency diff review](supply-chain/sbom-dependency-diff-review.md) +| Maturity | Meaning | +| --- | --- | +| `draft` | Useful hypothesis; evidence or implementation bridge is still incomplete. | +| `reviewed` | Structurally reviewed and supported, but not yet promoted as a stable library entry. | +| `stable` | Evidence-bounded, linked to a core implementation, and supported by at least one source note. | + +Every card records `maturity` and `last_reviewed` in front matter. ## Card Contract -Every stable card uses the same fields: +Every card uses the same body fields: 1. **Signal** 2. **Why it matters** -3. **False positives** -4. **Minimum evidence** +3. **False-positive contexts** +4. **Evidence limits** 5. **Defensive next step** -6. **Related project** -7. **Related notes** +6. **Related implementation** +7. **Supporting notes** -The minimum-evidence field is the claim boundary: a card should guide an +The evidence-limits field is the claim boundary: a card should guide an investigation without turning a weak signal into an unsupported conclusion. +## Automated Contract + +`python scripts/check_pattern_library.py` verifies that: + +* every card has valid maturity metadata and the fixed section order +* every stable card links to an approved core implementation +* every stable card links to at least one source note under `notes/` or `TryHackMe/` +* the featured set contains between six and eight stable cards +* every configured flagship case study links back to at least one pattern +* the extraction metric matches the current stable-card provenance graph + +## Flagship Case Studies + +* [ContAInment](../notes/80-blue-team/containment-public.md) +* [AI Forensics](../notes/80-blue-team/ai-forensics-public.md) +* [Understanding AI Supply Chains](../notes/80-blue-team/understanding-ai-supply-chains-public.md) + ## Project Bridges | Pattern area | Related project | | --- | --- | -| Authentication | [LogLens](https://github.com/stacknil/LogLens) | -| Detection engineering | [telemetry-lab](https://github.com/stacknil/telemetry-lab) | -| Cloud review | [telemetry-lab](https://github.com/stacknil/telemetry-lab) | +| Parsing and authentication | [LogLens](https://github.com/stacknil/LogLens) | +| Detection and human review | [telemetry-lab](https://github.com/stacknil/telemetry-lab) | +| Repository governance | [repo-sentinel-lite](https://github.com/stacknil/repo-sentinel-lite) | | Supply-chain review | [sbom-diff-and-risk](https://github.com/stacknil/scientific-computing-toolkit/tree/main/tools/sbom-diff-and-risk) | diff --git a/patterns/ai/human-verification-boundary.md b/patterns/ai/human-verification-boundary.md new file mode 100644 index 0000000..f9e640b --- /dev/null +++ b/patterns/ai/human-verification-boundary.md @@ -0,0 +1,68 @@ +--- +maturity: stable +last_reviewed: 2026-07-05 +--- + +# Human Verification Boundary + +## Signal + +What evidence appears? + +An AI-assisted workflow produces a classification, summary, recovered clue, or +draft finding that could influence an investigation or response decision. + +Common evidence: + +* model-generated triage labels or case summaries +* AI-selected anomalies or suspicious artefacts +* a proposed timeline assembled from multiple sources +* a decoded or reconstructed candidate result +* an AI output queued for analyst approval + +## Why it matters + +What risk does it suggest? + +AI can reduce review time, but non-determinism, weak source data, prompt +injection, and plausible false conclusions can move uncertainty into the final +case record. Human verification is the boundary between assistance and verdict. + +## False-positive contexts + +When can it be benign? + +* the output is clearly labeled as a draft +* deterministic rules already selected the bounded evidence +* the model only reformats analyst-approved facts +* a human independently checks every cited artefact +* rejected outputs remain visible in an audit trail + +## Evidence limits + +What must be present before making a claim? + +Preserve the source artefacts, deterministic pre-model selection, model input +boundary, output schema, validation result, and human decision. + +Do not treat fluent model output as evidence. The final claim must be traceable +to independently reviewable artefacts, and the accountable decision must remain +human-owned. + +## Defensive next step + +What should a defender check next? + +Reproduce the finding from original evidence, inspect rejected or alternate +outputs, check for prompt-injection exposure, and record the analyst's approval, +correction, or rejection. + +## Related implementation + +[telemetry-lab AI-assisted detection reviewer pack](https://github.com/stacknil/telemetry-lab/blob/main/docs/ai-assisted-detection-reviewer-pack.md) + +## Supporting notes + +* [AI Forensics](../../notes/80-blue-team/ai-forensics-public.md) +* [ContAInment](../../notes/80-blue-team/containment-public.md) +* [Prompt Defence](../../notes/80-blue-team/prompt-defence-public.md) diff --git a/patterns/auth/authentication-burst-triage.md b/patterns/auth/authentication-burst-triage.md new file mode 100644 index 0000000..2ab8fd0 --- /dev/null +++ b/patterns/auth/authentication-burst-triage.md @@ -0,0 +1,67 @@ +--- +maturity: stable +last_reviewed: 2026-07-05 +--- + +# Authentication Burst Triage + +## Signal + +What evidence appears? + +Authentication failures cluster around one source, account, host, or service +inside a bounded time window. + +Common evidence: + +* repeated terminal authentication failures +* a threshold crossed inside a documented window +* failures concentrated on one source or target +* username spread within the same burst +* a later success event near the failure cluster + +## Why it matters + +What risk does it suggest? + +The burst can indicate brute-force guessing, password spraying, stale +automation, or concentrated operator activity. It is a triage signal that +helps prioritize review; it is not a compromise verdict. + +## False-positive contexts + +When can it be benign? + +* a user repeatedly enters an old password +* automation continues using rotated credentials +* an approved scanner tests authentication controls +* a shared proxy combines unrelated user failures +* a lab or replay fixture intentionally crosses the threshold + +## Evidence limits + +What must be present before making a claim? + +Show the event type, source and target identities, failure count, threshold, +time window, username distribution, and nearby success outcomes. + +Do not infer successful credential compromise from failures alone. A later +success must be joined by source, account, session, or other defensible context +before it is treated as related. + +## Defensive next step + +What should a defender check next? + +Review nearby successes, MFA outcomes, account lockouts, session activity, and +known scanner or automation inventories. Preserve parser warnings that could +change the observed count. + +## Related implementation + +[LogLens Linux authentication case study](https://github.com/stacknil/LogLens/blob/main/docs/case-study-linux-auth-bruteforce.md) + +## Supporting notes + +* [Logs Fundamentals](../../notes/80-blue-team/logs-fundamentals.md) +* [Incident Response Fundamentals](../../notes/80-blue-team/incident-response-fundamentals.md) diff --git a/patterns/auth/multi-user-probing.md b/patterns/auth/multi-user-probing.md index a385ad1..db3cf5e 100644 --- a/patterns/auth/multi-user-probing.md +++ b/patterns/auth/multi-user-probing.md @@ -1,3 +1,8 @@ +--- +maturity: stable +last_reviewed: 2026-07-05 +--- + # Multi-User Probing ## Signal @@ -24,7 +29,7 @@ stuffing, or preparation for a later targeted login attempt. It is often quieter than single-account brute force because each account may see only a few failures. -## False positives +## False-positive contexts When can it be benign? @@ -34,7 +39,7 @@ When can it be benign? * a shared NAT or proxy groups unrelated users behind one source * a helpdesk workflow checks multiple locked or disabled accounts -## Minimum evidence +## Evidence limits What must be present before making a claim? @@ -53,11 +58,11 @@ Group the attempts by source, username, and service. Then check for later successful logins, lockouts, MFA challenges, impossible travel, and whether the same source probed other exposed services. -## Related project +## Related implementation [LogLens](https://github.com/stacknil/LogLens) -## Related notes +## Supporting notes * [Logs Fundamentals](../../notes/80-blue-team/logs-fundamentals.md) * [SOC Fundamentals](../../notes/80-blue-team/soc-fundamentals.md) diff --git a/patterns/auth/ssh-bruteforce-triage.md b/patterns/auth/ssh-bruteforce-triage.md index a7068d3..2890ad7 100644 --- a/patterns/auth/ssh-bruteforce-triage.md +++ b/patterns/auth/ssh-bruteforce-triage.md @@ -1,3 +1,8 @@ +--- +maturity: reviewed +last_reviewed: 2026-07-05 +--- + # SSH Brute-Force Triage ## Signal @@ -24,7 +29,7 @@ guessing, or an attacker trying to confirm which accounts exist. A successful login after the failure burst raises the priority because it may indicate credential compromise. -## False positives +## False-positive contexts When can it be benign? @@ -34,7 +39,7 @@ When can it be benign? * a monitoring system tests service availability * a known administrator reconnects repeatedly during maintenance -## Minimum evidence +## Evidence limits What must be present before making a claim? @@ -54,11 +59,11 @@ Check whether any attempted account had a successful SSH login near the same time, then review session commands, source reputation, MFA status, account age, and whether the source has appeared in prior authentication alerts. -## Related project +## Related implementation [LogLens](https://github.com/stacknil/LogLens) -## Related notes +## Supporting notes * [Logs Fundamentals](../../notes/80-blue-team/logs-fundamentals.md) * [Incident Response Fundamentals](../../notes/80-blue-team/incident-response-fundamentals.md) diff --git a/patterns/auth/sudo-burst-review.md b/patterns/auth/sudo-burst-review.md index 0d97ba5..95b8662 100644 --- a/patterns/auth/sudo-burst-review.md +++ b/patterns/auth/sudo-burst-review.md @@ -1,3 +1,8 @@ +--- +maturity: reviewed +last_reviewed: 2026-07-05 +--- + # Sudo Burst Review ## Signal @@ -23,7 +28,7 @@ The pattern may indicate privilege discovery, post-compromise enumeration, hands-on-keyboard activity, or an operator trying to convert user access into administrative control. -## False positives +## False-positive contexts When can it be benign? @@ -33,7 +38,7 @@ When can it be benign? * a developer debugging local service permissions * configuration management running under a human account -## Minimum evidence +## Evidence limits What must be present before making a claim? @@ -52,11 +57,11 @@ Review the full session timeline around the burst. Confirm whether the commands changed persistence, credentials, services, logs, network tools, or sensitive files, and compare the behaviour against the user's normal administrative role. -## Related project +## Related implementation [LogLens](https://github.com/stacknil/LogLens) -## Related notes +## Supporting notes * [Logs Fundamentals](../../notes/80-blue-team/logs-fundamentals.md) * [Incident Response Fundamentals](../../notes/80-blue-team/incident-response-fundamentals.md) diff --git a/patterns/cloud/iam-policy-attachment-review.md b/patterns/cloud/iam-policy-attachment-review.md index 9903396..03d2955 100644 --- a/patterns/cloud/iam-policy-attachment-review.md +++ b/patterns/cloud/iam-policy-attachment-review.md @@ -1,3 +1,8 @@ +--- +maturity: reviewed +last_reviewed: 2026-07-05 +--- + # IAM Policy Attachment Review ## Signal @@ -24,7 +29,7 @@ Policy attachment can immediately expand what an identity can read, change, or destroy. Suspicious attachments may indicate privilege escalation, persistence, lateral movement preparation, or accidental over-permissioning. -## False positives +## False-positive contexts When can it be benign? @@ -34,7 +39,7 @@ When can it be benign? * temporary incident response access * managed service or cloud control plane updating expected permissions -## Minimum evidence +## Evidence limits What must be present before making a claim? @@ -55,10 +60,10 @@ recent API calls by the actor and target identity, associated change tickets, infrastructure commits, and whether the policy should be rolled back or scoped down. -## Related project +## Related implementation [telemetry-lab](https://github.com/stacknil/telemetry-lab) -## Related notes +## Supporting notes * [Cloud Security Pitfalls](../../notes/80-blue-team/40-cloud/cloud-security-pitfalls.md) diff --git a/patterns/detection/alert-deduplication.md b/patterns/detection/alert-deduplication.md index 0c2cad8..2c4a031 100644 --- a/patterns/detection/alert-deduplication.md +++ b/patterns/detection/alert-deduplication.md @@ -1,3 +1,8 @@ +--- +maturity: stable +last_reviewed: 2026-07-05 +--- + # Alert Deduplication ## Signal @@ -23,7 +28,7 @@ Alert duplication can inflate severity, overwhelm analysts, and hide the real shape of an incident. Deduplication helps preserve one investigation thread without losing the supporting evidence. -## False positives +## False-positive contexts When can it be benign? @@ -33,7 +38,7 @@ When can it be benign? * alerts look similar but involve different users, assets, or outcomes * a detection rule intentionally fires per affected entity -## Minimum evidence +## Evidence limits What must be present before making a claim? @@ -51,11 +56,11 @@ Create or review the incident-level grouping key, then confirm that the deduplicated alert still keeps representative timestamps, entities, evidence links, and severity reasoning. -## Related project +## Related implementation [telemetry-lab](https://github.com/stacknil/telemetry-lab) -## Related notes +## Supporting notes * [Logs Fundamentals](../../notes/80-blue-team/logs-fundamentals.md) * [Report Writing for SOC L2](../../notes/80-blue-team/soc-l2-report-writing.md) diff --git a/patterns/detection/bounded-correlation.md b/patterns/detection/bounded-correlation.md index 3ee970d..6fe2bd3 100644 --- a/patterns/detection/bounded-correlation.md +++ b/patterns/detection/bounded-correlation.md @@ -1,3 +1,8 @@ +--- +maturity: stable +last_reviewed: 2026-07-05 +--- + # Bounded Correlation ## Signal @@ -23,7 +28,7 @@ Bounded correlation helps detect behaviour that individual rules miss. It also keeps detection logic honest by requiring a specific relationship rather than loosely connecting unrelated events. -## False positives +## False-positive contexts When can it be benign? @@ -33,7 +38,7 @@ When can it be benign? * the time window is too broad and catches unrelated events * entity mapping is weak, such as shared IPs or reused hostnames -## Minimum evidence +## Evidence limits What must be present before making a claim? @@ -52,11 +57,11 @@ Validate the join key and time window against known benign workflows. Then review whether the correlated sequence produces a clear investigation action, such as session review, host isolation decision, or identity reset. -## Related project +## Related implementation [telemetry-lab](https://github.com/stacknil/telemetry-lab) -## Related notes +## Supporting notes * [Logs Fundamentals](../../notes/80-blue-team/logs-fundamentals.md) * [Incident Response Fundamentals](../../notes/80-blue-team/incident-response-fundamentals.md) diff --git a/patterns/detection/cooldown-suppression.md b/patterns/detection/cooldown-suppression.md index 2fd3141..af21e0e 100644 --- a/patterns/detection/cooldown-suppression.md +++ b/patterns/detection/cooldown-suppression.md @@ -1,3 +1,8 @@ +--- +maturity: reviewed +last_reviewed: 2026-07-05 +--- + # Cooldown Suppression ## Signal @@ -23,7 +28,7 @@ Cooldown suppression reduces alert fatigue, but it can also hide repeated activity if the suppression key is too broad or the cooldown window is too long. The control should reduce noise without erasing escalation signals. -## False positives +## False-positive contexts When can it be benign? @@ -33,7 +38,7 @@ When can it be benign? * one benign automation job retriggering the same condition * expected retry behaviour from a monitored service -## Minimum evidence +## Evidence limits What must be present before making a claim? @@ -52,11 +57,11 @@ Review suppressed events for changes in user, host, destination, success state, or command behaviour. Tune the cooldown key so repeated identical noise is suppressed while meaningful changes still create a new alert. -## Related project +## Related implementation [telemetry-lab](https://github.com/stacknil/telemetry-lab) -## Related notes +## Supporting notes * [Logs Fundamentals](../../notes/80-blue-team/logs-fundamentals.md) * [SOC Fundamentals](../../notes/80-blue-team/soc-fundamentals.md) diff --git a/patterns/parsing/parser-uncertainty.md b/patterns/parsing/parser-uncertainty.md new file mode 100644 index 0000000..0110ab9 --- /dev/null +++ b/patterns/parsing/parser-uncertainty.md @@ -0,0 +1,67 @@ +--- +maturity: stable +last_reviewed: 2026-07-05 +--- + +# Parser Uncertainty + +## Signal + +What evidence appears? + +A parser reports unsupported, malformed, or unclassified input alongside the +records it successfully normalized. + +Common evidence: + +* non-zero unparsed or warning counts +* line-numbered parser warnings +* unknown-message categories or pattern buckets +* a parse-success rate below 100 percent +* source lines excluded from downstream detection input + +## Why it matters + +What risk does it suggest? + +Silent parser gaps can make missing detections look like negative security +evidence. Visible uncertainty lets a reviewer distinguish "the rule did not +match" from "the parser did not understand the input." + +## False-positive contexts + +When can it be benign? + +* a new but harmless log format appears after an upgrade +* blank, comment, or health-check lines are intentionally ignored +* a synthetic fixture includes malformed records for testing +* a supported program emits an irrelevant message variant +* mixed log sources are sent to a parser with a deliberately narrow scope + +## Evidence limits + +What must be present before making a claim? + +Show total, parsed, skipped, and unparsed counts; representative source lines; +the parser category; and whether unsupported lines entered detector input. + +Do not claim that an event was absent when relevant input remained unparsed. +Parser telemetry describes coverage boundaries, not attacker intent or system +compromise. + +## Defensive next step + +What should a defender check next? + +Sample the unsupported records, decide whether they are relevant to the +investigation, and either add a tested parser case or document why the input +remains outside the supported contract. + +## Related implementation + +[LogLens parser uncertainty case study](https://github.com/stacknil/LogLens/blob/main/docs/case-study-parser-uncertainty-as-evidence.md) + +## Supporting notes + +* [Logs Fundamentals](../../notes/80-blue-team/logs-fundamentals.md) +* [AI Forensics](../../notes/80-blue-team/ai-forensics-public.md) diff --git a/patterns/repository/repository-baseline-governance.md b/patterns/repository/repository-baseline-governance.md new file mode 100644 index 0000000..d98c32b --- /dev/null +++ b/patterns/repository/repository-baseline-governance.md @@ -0,0 +1,68 @@ +--- +maturity: stable +last_reviewed: 2026-07-05 +--- + +# Repository Baseline Governance + +## Signal + +What evidence appears? + +A committed baseline, allowlist, or suppression file changes the findings that +a repository quality or security check reports. + +Common evidence: + +* new suppression entries +* removed or stale baseline entries +* changed fingerprints or rule identifiers +* broad path or rule allowlists +* a clean CI result after the baseline changed + +## Why it matters + +What risk does it suggest? + +Baselines preserve reviewed exceptions, but they can also hide new findings or +turn temporary debt into permanent policy. Baseline drift is security-adjacent +code and should remain explicit and reviewable. + +## False-positive contexts + +When can it be benign? + +* a known fixture is intentionally retained +* a finding disappears after the underlying issue is fixed +* paths move without changing the reviewed evidence +* a rule version changes its fingerprint format +* a narrowly scoped exception documents expected generated content + +## Evidence limits + +What must be present before making a claim? + +Show the baseline diff, affected rule, path or fingerprint, current scan +result, suppression scope, and reviewer rationale. + +A baseline entry does not prove that a finding is safe. A green check after a +baseline update only proves that current findings were evaluated against the +new suppression state. + +## Defensive next step + +What should a defender check next? + +Compare scans with and without the baseline, classify active, stale, +ambiguous, and unmatched entries, and require an explanation for every added +or broadened suppression. + +## Related implementation + +[repo-sentinel-lite baseline review guide](https://github.com/stacknil/repo-sentinel-lite/blob/main/docs/baseline-review.md) + +## Supporting notes + +* [Vulnerability Scanner Overview](../../notes/80-blue-team/vulnerability-scanner-overview.md) +* [Maintenance Checkpoint](../../docs/maintenance-checkpoint.md) +* [Publication Workflow](../../docs/publication-workflow.md) diff --git a/patterns/supply-chain/sbom-dependency-diff-review.md b/patterns/supply-chain/sbom-dependency-diff-review.md index 216904d..4d088aa 100644 --- a/patterns/supply-chain/sbom-dependency-diff-review.md +++ b/patterns/supply-chain/sbom-dependency-diff-review.md @@ -1,3 +1,8 @@ +--- +maturity: reviewed +last_reviewed: 2026-07-05 +--- + # SBOM Dependency Diff Review ## Signal @@ -23,7 +28,7 @@ Dependency diffs reveal when the software supply chain changed. A small diff can introduce exploitable vulnerabilities, malicious packages, dependency confusion exposure, license risk, or unexpected runtime behaviour. -## False positives +## False-positive contexts When can it be benign? @@ -33,7 +38,7 @@ When can it be benign? * build tool normalization changing output order * environment-specific optional dependencies -## Minimum evidence +## Evidence limits What must be present before making a claim? @@ -53,11 +58,11 @@ Review the dependency's source repository, package registry metadata, release notes, maintainer history, vulnerability data, license, and whether the change matches the intended pull request or release plan. -## Related project +## Related implementation [sbom-diff-and-risk](https://github.com/stacknil/scientific-computing-toolkit/tree/main/tools/sbom-diff-and-risk) -## Related notes +## Supporting notes * [Understanding AI Supply Chains](../../notes/80-blue-team/understanding-ai-supply-chains-public.md) * [Vulnerabilities 101](../../notes/10-web/foundations/vulnerabilities-101-public.md) diff --git a/patterns/supply-chain/sbom-policy-warning.md b/patterns/supply-chain/sbom-policy-warning.md new file mode 100644 index 0000000..bfb976e --- /dev/null +++ b/patterns/supply-chain/sbom-policy-warning.md @@ -0,0 +1,66 @@ +--- +maturity: stable +last_reviewed: 2026-07-05 +--- + +# SBOM Policy Warning + +## Signal + +What evidence appears? + +A dependency diff matches a local policy rule and produces a warning without a +blocking decision. + +Common evidence: + +* an added, removed, or changed package +* a named policy rule matched to the component +* warning severity recorded in JSON, Markdown, or SARIF +* a non-blocking process exit +* an explanation linking the diff to the policy decision + +## Why it matters + +What risk does it suggest? + +A warning identifies dependency change that requires human review. It makes +local policy visible without overstating the package as malicious, vulnerable, +or unsafe. + +## False-positive contexts + +When can it be benign? + +* an approved package is added for a documented feature +* a lockfile refresh introduces expected transitive changes +* an offline policy lacks vulnerability or repository context +* a policy intentionally warns on every new package +* the changed component is development-only or platform-specific + +## Evidence limits + +What must be present before making a claim? + +Show the before and after component state, matched policy rule, observed value, +severity source, decision reason, and whether the outcome blocks the build. + +Do not convert an offline policy warning into a CVE, malware, package-safety, +or current repository-reputation verdict. + +## Defensive next step + +What should a defender check next? + +Review why the dependency changed, its provenance and license, the intended +pull request, available vulnerability evidence, and whether local policy +should remain warning-only or become blocking. + +## Related implementation + +[sbom-diff-and-risk policy warning reviewer case](https://github.com/stacknil/scientific-computing-toolkit/blob/main/tools/sbom-diff-and-risk/docs/policy-warning-reviewer-case.md) + +## Supporting notes + +* [Understanding AI Supply Chains](../../notes/80-blue-team/understanding-ai-supply-chains-public.md) +* [Vulnerabilities 101](../../notes/10-web/foundations/vulnerabilities-101-public.md) diff --git a/schemas/pattern-library.json b/schemas/pattern-library.json new file mode 100644 index 0000000..7cfabe4 --- /dev/null +++ b/schemas/pattern-library.json @@ -0,0 +1,54 @@ +{ + "version": 1, + "maturity_values": [ + "draft", + "reviewed", + "stable" + ], + "required_sections": [ + "Signal", + "Why it matters", + "False-positive contexts", + "Evidence limits", + "Defensive next step", + "Related implementation", + "Supporting notes" + ], + "core_projects": [ + { + "name": "LogLens", + "url_prefix": "https://github.com/stacknil/LogLens" + }, + { + "name": "telemetry-lab", + "url_prefix": "https://github.com/stacknil/telemetry-lab" + }, + { + "name": "repo-sentinel-lite", + "url_prefix": "https://github.com/stacknil/repo-sentinel-lite" + }, + { + "name": "sbom-diff-and-risk", + "url_prefix": "https://github.com/stacknil/scientific-computing-toolkit" + } + ], + "featured_patterns": [ + "patterns/parsing/parser-uncertainty.md", + "patterns/auth/authentication-burst-triage.md", + "patterns/auth/multi-user-probing.md", + "patterns/detection/alert-deduplication.md", + "patterns/detection/bounded-correlation.md", + "patterns/repository/repository-baseline-governance.md", + "patterns/supply-chain/sbom-policy-warning.md", + "patterns/ai/human-verification-boundary.md" + ], + "flagship_case_studies": [ + "notes/80-blue-team/containment-public.md", + "notes/80-blue-team/ai-forensics-public.md", + "notes/80-blue-team/understanding-ai-supply-chains-public.md" + ], + "metric_surfaces": [ + "README.md", + "patterns/README.md" + ] +} diff --git a/scripts/check_pattern_library.py b/scripts/check_pattern_library.py new file mode 100644 index 0000000..e16d172 --- /dev/null +++ b/scripts/check_pattern_library.py @@ -0,0 +1,249 @@ +#!/usr/bin/env python3 +from __future__ import annotations + +import json +import re +import sys +from dataclasses import dataclass +from datetime import date +from pathlib import Path +from urllib.parse import unquote, urlparse + +import yaml + + +ROOT = Path(__file__).resolve().parents[1] +CONFIG_PATH = ROOT / "schemas" / "pattern-library.json" +FRONT_MATTER_RE = re.compile(r"\A---\n(.*?)\n---\n", re.DOTALL) +HEADING_RE = re.compile(r"^## (.+)$", re.MULTILINE) +LINK_RE = re.compile(r"\[[^\]]+\]\(([^)]+)\)") + + +@dataclass(frozen=True) +class Card: + path: Path + maturity: str + last_reviewed: date + sections: dict[str, str] + + +def relative(path: Path) -> str: + return path.relative_to(ROOT).as_posix() + + +def load_config() -> dict[str, object]: + return json.loads(CONFIG_PATH.read_text(encoding="utf-8")) + + +def parse_card( + path: Path, + required_sections: list[str], + maturity_values: set[str], + errors: list[str], +) -> Card | None: + text = path.read_text(encoding="utf-8").replace("\r\n", "\n") + match = FRONT_MATTER_RE.match(text) + if match is None: + errors.append(f"{relative(path)}: missing YAML front matter") + return None + + metadata = yaml.safe_load(match.group(1)) or {} + maturity = metadata.get("maturity") + if maturity not in maturity_values: + errors.append( + f"{relative(path)}: maturity must be one of " + f"{', '.join(sorted(maturity_values))}" + ) + return None + + reviewed_value = metadata.get("last_reviewed") + try: + reviewed = date.fromisoformat(str(reviewed_value)) + except (TypeError, ValueError): + errors.append(f"{relative(path)}: last_reviewed must use YYYY-MM-DD") + return None + + body = text[match.end() :] + headings = list(HEADING_RE.finditer(body)) + heading_names = [heading.group(1) for heading in headings] + if heading_names != required_sections: + errors.append( + f"{relative(path)}: expected sections {required_sections}, " + f"found {heading_names}" + ) + return None + + sections: dict[str, str] = {} + for index, heading in enumerate(headings): + end = headings[index + 1].start() if index + 1 < len(headings) else len(body) + content = body[heading.end() : end].strip() + if not content: + errors.append(f"{relative(path)}: empty section {heading.group(1)!r}") + sections[heading.group(1)] = content + + return Card(path, maturity, reviewed, sections) + + +def links(text: str) -> list[str]: + return [match.group(1).strip() for match in LINK_RE.finditer(text)] + + +def resolve_local_link(source: Path, target: str) -> Path | None: + parsed = urlparse(target) + if parsed.scheme or target.startswith(("#", "mailto:")): + return None + + link_path = unquote(target.split("#", 1)[0]) + if not link_path: + return None + + resolved = (source.parent / link_path).resolve() + try: + resolved.relative_to(ROOT) + except ValueError: + return None + return resolved + + +def is_core_project_link(target: str, prefixes: list[str]) -> bool: + return any(target == prefix or target.startswith(f"{prefix}/") for prefix in prefixes) + + +def validate() -> tuple[list[str], int, int, int]: + config = load_config() + required_sections = list(config["required_sections"]) + maturity_values = set(config["maturity_values"]) + project_prefixes = [ + project["url_prefix"] for project in config["core_projects"] + ] + errors: list[str] = [] + + card_paths = sorted( + path + for path in (ROOT / "patterns").rglob("*.md") + if path.name != "README.md" + ) + cards = [ + card + for path in card_paths + if ( + card := parse_card( + path, + required_sections, + maturity_values, + errors, + ) + ) + is not None + ] + cards_by_path = {relative(card.path): card for card in cards} + stable_source_notes: set[Path] = set() + + for card in cards: + implementation_links = links(card.sections["Related implementation"]) + if not implementation_links: + errors.append(f"{relative(card.path)}: missing related implementation link") + + supporting_links = links(card.sections["Supporting notes"]) + local_supporting_notes: list[Path] = [] + for target in supporting_links: + resolved = resolve_local_link(card.path, target) + if resolved is None: + continue + if not resolved.is_file(): + errors.append( + f"{relative(card.path)}: supporting note does not exist: {target}" + ) + continue + if resolved.suffix.lower() != ".md": + errors.append( + f"{relative(card.path)}: supporting note is not Markdown: {target}" + ) + continue + local_supporting_notes.append(resolved) + + if not local_supporting_notes: + errors.append(f"{relative(card.path)}: missing local supporting note") + + if card.maturity == "stable": + source_notes = [ + path + for path in local_supporting_notes + if relative(path).startswith(("notes/", "TryHackMe/")) + ] + if not source_notes: + errors.append( + f"{relative(card.path)}: stable card must link to a source note" + ) + if not any( + is_core_project_link(target, project_prefixes) + for target in implementation_links + ): + errors.append( + f"{relative(card.path)}: stable card must link to a core project" + ) + stable_source_notes.update(source_notes) + + featured_paths = list(config["featured_patterns"]) + if not 6 <= len(featured_paths) <= 8: + errors.append("featured_patterns must contain between 6 and 8 cards") + if len(featured_paths) != len(set(featured_paths)): + errors.append("featured_patterns contains duplicate paths") + for path in featured_paths: + card = cards_by_path.get(path) + if card is None: + errors.append(f"featured pattern does not exist: {path}") + elif card.maturity != "stable": + errors.append(f"featured pattern is not stable: {path}") + + card_path_set = set(card_paths) + for case_path_value in config["flagship_case_studies"]: + case_path = ROOT / case_path_value + if not case_path.is_file(): + errors.append(f"flagship case study does not exist: {case_path_value}") + continue + case_targets = { + resolved + for target in links(case_path.read_text(encoding="utf-8")) + if (resolved := resolve_local_link(case_path, target)) is not None + } + if not case_targets.intersection(card_path_set): + errors.append( + f"{case_path_value}: flagship case study must link to a pattern" + ) + + stable_count = sum(card.maturity == "stable" for card in cards) + supporting_count = len(stable_source_notes) + metric = ( + f"Current extraction metric: **{stable_count} stable reusable security " + f"patterns extracted from {supporting_count} distinct supporting notes**." + ) + for surface_value in config["metric_surfaces"]: + surface = ROOT / surface_value + if not surface.is_file(): + errors.append(f"metric surface does not exist: {surface_value}") + elif re.sub(r"\s+", " ", metric) not in re.sub( + r"\s+", " ", surface.read_text(encoding="utf-8") + ): + errors.append(f"{surface_value}: expected metric line: {metric}") + + return errors, len(cards), stable_count, supporting_count + + +def main() -> int: + errors, card_count, stable_count, supporting_count = validate() + if errors: + for error in errors: + print(f"ERROR: {error}", file=sys.stderr) + return 1 + + print( + "Pattern library is valid: " + f"{stable_count} stable patterns from {supporting_count} supporting notes " + f"across {card_count} total cards." + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/render_readme_snapshot.py b/scripts/render_readme_snapshot.py index 4264f45..c9a24aa 100644 --- a/scripts/render_readme_snapshot.py +++ b/scripts/render_readme_snapshot.py @@ -70,11 +70,11 @@ def build_at_a_glance(files: list[str]) -> str: lines = [ "## At A Glance", "", - "Current public snapshot:", + "Supporting source archive:", "", - f"- `{total_notes}` active public notes", - f"- `{tryhackme_notes}` active TryHackMe notes", - f"- `{notes_tree_notes}` topic-organized notes under `notes/`", + f"- `{total_notes}` public notes retained as source material", + f"- `{tryhackme_notes}` TryHackMe source notes", + f"- `{notes_tree_notes}` topic-organized source notes under `notes/`", f"- `{len(tryhackme_tracks)}` organized TryHackMe learning tracks", "- public governance for taxonomy, placeholders, publication, and maintenance", "", @@ -98,7 +98,7 @@ def build_what_you_will_find(files: list[str]) -> str: lines = [ "## What You Will Find", "", - "The live public corpus is currently centered on **TryHackMe** and supplemented by a smaller topic-organized `notes/` tree.", + "The supporting source corpus is centered on **TryHackMe** and supplemented by a topic-organized `notes/` tree.", "", "| Track | Notes | Focus |", "| --- | --- | --- |", @@ -109,7 +109,7 @@ def build_what_you_will_find(files: list[str]) -> str: lines.extend( [ "", - "Additional organized notes outside the TryHackMe tree:", + "Additional source notes outside the TryHackMe tree:", "", f"- `notes/` currently contains `{notes_tree_notes}` active public notes", "", diff --git a/scripts/run_markdownlint.py b/scripts/run_markdownlint.py index c4c74b7..ff92554 100644 --- a/scripts/run_markdownlint.py +++ b/scripts/run_markdownlint.py @@ -15,7 +15,7 @@ def is_markdown_path(value: str) -> bool: return value.lower().endswith(".md") -def is_active_note_path(value: str) -> bool: +def is_blocking_markdown_path(value: str) -> bool: path = Path(value) if not path.is_absolute(): path = ROOT / path @@ -25,9 +25,12 @@ def is_active_note_path(value: str) -> bool: except (OSError, ValueError): return False + if not rel_path.endswith(".md"): + return False + if rel_path == "README.md" or rel_path.startswith("patterns/"): + return True return ( - rel_path.endswith(".md") - and (rel_path.startswith("TryHackMe/") or rel_path.startswith("notes/")) + (rel_path.startswith("TryHackMe/") or rel_path.startswith("notes/")) and not rel_path.startswith("TryHackMe/_meta/") ) @@ -67,13 +70,13 @@ def main() -> int: filtered_files: list[str] = [] for arg in args: if is_markdown_path(arg): - if is_active_note_path(arg): + if is_blocking_markdown_path(arg): filtered_files.append(arg) else: passthrough_args.append(arg) if not filtered_files: - print("No active-note Markdown files provided to markdownlint; skipping.") + print("No blocking-scope Markdown files provided; skipping markdownlint.") return 0 if "--config" not in passthrough_args and "-c" not in passthrough_args: