mirror of
https://github.com/iimp0ster/detection-chokepoints
synced 2026-08-09 12:41:00 +00:00
Add Jekyll/GitHub Pages website with dark-theme search UI
- Jekyll 4 site with dark security theme (bg:#0d1117, accent:#f0883e) - scripts/aggregate.py: pre-build script reads chokepoints YAML + sigma rules, outputs _data/chokepoints.yml, search-index.json, _chokepoints stubs - _layouts/default.html + chokepoint.html: base layout and detail page with tabbed sigma panels (Research/Hunt/Analyst) and copy buttons - _includes/nav.html + chokepoint-card.html: reusable components - index.html: home page with Fuse.js fuzzy search, tactic/priority filter chips, URL query string state, and keyboard shortcut (/) - assets/css/style.css: responsive 3→2→1 col grid, priority colour coding, card hover effects, detail page with timeline/bypass/OSINT sections - assets/js/search.js: Fuse.js client-side search with AND filter logic - assets/js/fuse.min.js: bundled Fuse.js v7.0.0 (no CDN dependency) - .github/workflows/pages.yml: CI pipeline — aggregate → jekyll build → deploy - .gitignore: exclude _site/, generated _data/ and _chokepoints/ stubs - Fix YAML parse error in ransomware-service-manipulation.yml (unquoted colon) - _config.yml: exclude source YAML dirs from Jekyll output Site will be live at https://iimp0ster.github.io/detection-chokepoints/ once GitHub Pages is enabled (Settings → Pages → Source: GitHub Actions). https://claude.ai/code/session_01LWLhVRq5vYHXiDKn86PXhr
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
name: Build and Deploy to GitHub Pages
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [master, main]
|
||||
# Allow manual trigger from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.11"
|
||||
|
||||
- name: Install Python dependencies
|
||||
run: pip install pyyaml
|
||||
|
||||
- name: Aggregate chokepoint data
|
||||
run: python scripts/aggregate.py
|
||||
|
||||
- name: Set up Ruby
|
||||
uses: ruby/setup-ruby@v1
|
||||
with:
|
||||
ruby-version: "3.3"
|
||||
bundler-cache: true # runs `bundle install` and caches gems
|
||||
|
||||
- name: Configure GitHub Pages
|
||||
uses: actions/configure-pages@v5
|
||||
|
||||
- name: Build Jekyll site
|
||||
run: bundle exec jekyll build --baseurl "/detection-chokepoints"
|
||||
env:
|
||||
JEKYLL_ENV: production
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v3
|
||||
with:
|
||||
path: "_site"
|
||||
|
||||
deploy:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
_site/
|
||||
.jekyll-cache/
|
||||
.jekyll-metadata
|
||||
.sass-cache/
|
||||
Gemfile.lock
|
||||
vendor/
|
||||
node_modules/
|
||||
|
||||
# Generated by aggregate.py (CI creates these at build time)
|
||||
_data/chokepoints.yml
|
||||
assets/js/search-index.json
|
||||
_chokepoints/
|
||||
@@ -0,0 +1,7 @@
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "jekyll", "~> 4.3"
|
||||
gem "jekyll-seo-tag", "~> 2.8"
|
||||
gem "kramdown-parser-gfm", "~> 1.1"
|
||||
gem "rouge", "~> 4.2"
|
||||
gem "webrick", "~> 1.8" # needed for `jekyll serve` on Ruby 3+
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
title: Detection Chokepoints
|
||||
tagline: "TTPs evolve. Chokepoints don't."
|
||||
description: >
|
||||
A community database of attack prerequisites — the conditions attackers cannot avoid
|
||||
regardless of tool choice. Each chokepoint includes Sigma detection rules at Research,
|
||||
Hunt, and Analyst maturity levels.
|
||||
|
||||
baseurl: "/detection-chokepoints"
|
||||
url: "https://iimp0ster.github.io"
|
||||
github_username: iimp0ster
|
||||
github_repo: detection-chokepoints
|
||||
|
||||
markdown: kramdown
|
||||
highlighter: rouge
|
||||
|
||||
kramdown:
|
||||
input: GFM
|
||||
hard_wrap: false
|
||||
syntax_highlighter: rouge
|
||||
|
||||
collections:
|
||||
chokepoints:
|
||||
output: true
|
||||
permalink: /chokepoints/:name/
|
||||
|
||||
plugins:
|
||||
- jekyll-seo-tag
|
||||
|
||||
# Files/dirs Jekyll should not copy or process into _site/
|
||||
exclude:
|
||||
- Gemfile
|
||||
- Gemfile.lock
|
||||
- scripts/
|
||||
- schema/
|
||||
- templates/
|
||||
- chokepoints/
|
||||
- sigma-rules/
|
||||
- attack-chains/
|
||||
- trends/
|
||||
- intel/
|
||||
- README.md
|
||||
- CONTRIBUTING.md
|
||||
- CHANGELOG.md
|
||||
- FRAMEWORK.md
|
||||
- vendor/
|
||||
- node_modules/
|
||||
- "*.gemspec"
|
||||
- ".github/"
|
||||
@@ -0,0 +1,39 @@
|
||||
{% assign cp = include.cp %}
|
||||
{% assign priority_class = cp.DetectionPriority | downcase %}
|
||||
<article class="chokepoint-card" data-tactic="{{ cp.Tactic | downcase }}" data-priority="{{ cp.DetectionPriority }}" data-difficulty="{{ cp.Difficulty }}">
|
||||
<a href="{{ cp._slug | prepend: '/chokepoints/' | append: '/' | relative_url }}" class="card-link" aria-label="View {{ cp.Name }}">
|
||||
<div class="card-header">
|
||||
<span class="priority-badge priority-{{ priority_class }}">{{ cp.DetectionPriority }}</span>
|
||||
{% if cp.Status and cp.Status != "Active" %}
|
||||
<span class="status-badge status-{{ cp.Status | downcase }}">{{ cp.Status }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h3 class="card-title">{{ cp.Name }}</h3>
|
||||
<p class="card-tactic">
|
||||
<svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M12 2L2 7l10 5 10-5-10-5zM2 17l10 5 10-5M2 12l10 5 10-5"/></svg>
|
||||
{{ cp.Tactic }}
|
||||
</p>
|
||||
<p class="card-description">{{ cp.Description | truncate: 120 }}</p>
|
||||
<div class="card-meta">
|
||||
{% if cp.MitreIds %}
|
||||
<div class="card-mitre">
|
||||
{% for mid in cp.MitreIds limit:3 %}
|
||||
<span class="mitre-chip">{{ mid }}</span>
|
||||
{% endfor %}
|
||||
{% if cp.MitreIds.size > 3 %}
|
||||
<span class="mitre-chip mitre-more">+{{ cp.MitreIds.size | minus: 3 }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if cp.Variations %}
|
||||
<span class="card-variants">{{ cp.Variations.size }} variation{% if cp.Variations.size != 1 %}s{% endif %}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if cp.TheConstant %}
|
||||
<div class="card-constant">
|
||||
<span class="constant-label">THE CONSTANT</span>
|
||||
<span class="constant-value">{{ cp.TheConstant | truncate: 80 }}</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</a>
|
||||
</article>
|
||||
@@ -0,0 +1,41 @@
|
||||
<nav class="site-nav" role="navigation" aria-label="Main navigation">
|
||||
<div class="nav-inner">
|
||||
<a class="nav-logo" href="{{ '/' | relative_url }}">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22" viewBox="0 0 24 24"
|
||||
fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"
|
||||
stroke-linejoin="round" aria-hidden="true">
|
||||
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
|
||||
</svg>
|
||||
<span>Detection Chokepoints</span>
|
||||
</a>
|
||||
|
||||
<ul class="nav-links">
|
||||
<li><a href="{{ '/' | relative_url }}"
|
||||
{% if page.url == '/' or page.url == '/index.html' %}class="active"{% endif %}>
|
||||
Chokepoints
|
||||
</a></li>
|
||||
<li><a href="https://github.com/{{ site.github_username }}/{{ site.github_repo }}/tree/main/attack-chains"
|
||||
target="_blank" rel="noopener">Attack Chains</a></li>
|
||||
<li><a href="https://github.com/{{ site.github_username }}/{{ site.github_repo }}/blob/main/FRAMEWORK.md"
|
||||
target="_blank" rel="noopener">Framework</a></li>
|
||||
<li><a href="https://github.com/{{ site.github_username }}/{{ site.github_repo }}/blob/main/CONTRIBUTING.md"
|
||||
target="_blank" rel="noopener">Contribute</a></li>
|
||||
<li>
|
||||
<a class="nav-github" href="https://github.com/{{ site.github_username }}/{{ site.github_repo }}"
|
||||
target="_blank" rel="noopener" aria-label="GitHub">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24"
|
||||
fill="currentColor" aria-hidden="true">
|
||||
<path d="M12 0C5.37 0 0 5.37 0 12c0 5.31 3.435 9.795 8.205 11.385.6.105.825-.255.825-.57
|
||||
0-.285-.015-1.23-.015-2.235-3.015.555-3.795-.735-4.035-1.41-.135-.345-.72-1.41-1.23-1.695
|
||||
-.42-.225-1.02-.78-.015-.795.945-.015 1.62.87 1.845 1.23 1.08 1.815 2.805 1.305 3.495.99
|
||||
.105-.78.42-1.305.765-1.605-2.67-.3-5.46-1.335-5.46-5.925 0-1.305.465-2.385 1.23-3.225
|
||||
-.12-.3-.54-1.53.12-3.18 0 0 1.005-.315 3.3 1.23.96-.27 1.98-.405 3-.405s2.04.135 3 .405
|
||||
c2.295-1.56 3.3-1.23 3.3-1.23.66 1.65.24 2.88.12 3.18.765.84 1.23 1.905 1.23 3.225
|
||||
0 4.605-2.805 5.625-5.475 5.925.435.375.81 1.095.81 2.22 0 1.605-.015 2.895-.015 3.3
|
||||
0 .315.225.69.825.57A12.02 12.02 0 0 0 24 12c0-6.63-5.37-12-12-12z"/>
|
||||
</svg>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
@@ -0,0 +1,319 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
{% assign cp = site.data.chokepoints | where: "_slug", page.slug | first %}
|
||||
|
||||
{% if cp == nil %}
|
||||
<div class="container">
|
||||
<p class="error">Chokepoint data not found for slug: {{ page.slug }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
|
||||
<article class="chokepoint-detail">
|
||||
<div class="container">
|
||||
|
||||
<!-- ── Header ─────────────────────────────────────────────────── -->
|
||||
<header class="detail-header">
|
||||
<div class="detail-header-top">
|
||||
<a href="{{ '/' | relative_url }}" class="back-link">← All Chokepoints</a>
|
||||
<span class="badge priority-{{ cp.DetectionPriority | downcase }}">{{ cp.DetectionPriority }}</span>
|
||||
</div>
|
||||
<h1 class="detail-title">{{ cp.Name }}</h1>
|
||||
|
||||
<div class="detail-meta">
|
||||
{% for tactic in cp.Tactics %}
|
||||
<span class="badge tactic-badge">{{ tactic }}</span>
|
||||
{% endfor %}
|
||||
{% for mid in cp.MitreIds %}
|
||||
<a class="badge mitre-badge"
|
||||
href="https://attack.mitre.org/techniques/{{ mid | replace: '.', '/' }}/"
|
||||
target="_blank" rel="noopener">{{ mid }}</a>
|
||||
{% endfor %}
|
||||
<span class="badge diff-badge diff-{{ cp.DetectionDifficulty | downcase }}">
|
||||
Detection difficulty: {{ cp.DetectionDifficulty }}
|
||||
</span>
|
||||
<span class="badge prev-badge">
|
||||
Prevalence: {{ cp.ThreatPrevalence }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<p class="detail-description">{{ cp.Description }}</p>
|
||||
|
||||
<p class="detail-byline">
|
||||
By {{ cp.Author }} · Updated {{ cp.LastUpdated }}
|
||||
· <a href="https://github.com/{{ site.github_username }}/{{ site.github_repo }}/blob/main/{{ cp._source_path }}"
|
||||
target="_blank" rel="noopener">View source YAML</a>
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- ── Prerequisites ─────────────────────────────────────────── -->
|
||||
<section class="detail-section">
|
||||
<h2>Prerequisites <span class="section-sub">(The Chokepoint)</span></h2>
|
||||
<p class="section-intro">What <strong>must</strong> be true for this technique to succeed,
|
||||
regardless of which tool is used:</p>
|
||||
<ul class="prereq-list">
|
||||
{% for prereq in cp.Prerequisites %}
|
||||
<li><span class="prereq-icon">✓</span> {{ prereq }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- ── Variations ─────────────────────────────────────────────── -->
|
||||
{% if cp.Variations %}
|
||||
<section class="detail-section">
|
||||
<h2>Variations</h2>
|
||||
<p class="section-intro">Tools and methods that exploit this chokepoint (the list grows; the chokepoint doesn't change):</p>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Tool / Method</th>
|
||||
<th>First Seen</th>
|
||||
<th>Status</th>
|
||||
<th>Notes</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for v in cp.Variations %}
|
||||
<tr>
|
||||
<td><strong>{{ v.Name }}</strong></td>
|
||||
<td>{{ v.FirstSeen }}</td>
|
||||
<td><span class="status-badge status-{{ v.Status | downcase }}">{{ v.Status }}</span></td>
|
||||
<td>{{ v.Notes }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- ── Detection Strategy ─────────────────────────────────────── -->
|
||||
<section class="detail-section">
|
||||
<h2>Detection Strategy</h2>
|
||||
<p class="section-intro">Build detections iteratively. Start broad to understand your
|
||||
baseline, then tighten to production-ready.</p>
|
||||
|
||||
{% assign levels = "Research,Hunt,Analyst" | split: "," %}
|
||||
<div class="sigma-tabs">
|
||||
<div class="tab-buttons" role="tablist">
|
||||
{% for level in levels %}
|
||||
{% assign level_lower = level | downcase %}
|
||||
<button class="tab-btn {% if level == 'Research' %}active{% endif %}"
|
||||
role="tab"
|
||||
data-tab="{{ level_lower }}"
|
||||
aria-selected="{% if level == 'Research' %}true{% else %}false{% endif %}">
|
||||
{{ level }}
|
||||
{% assign fp_map = "High,Medium,Low" | split: "," %}
|
||||
{% assign fp_idx = forloop.index0 %}
|
||||
<span class="tab-fp fp-{{ level_lower }}">{{ fp_map[fp_idx] }} FP</span>
|
||||
</button>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
{% for level in levels %}
|
||||
{% assign level_lower = level | downcase %}
|
||||
{% assign detection = cp.Detections | where: "Level", level | first %}
|
||||
{% assign sigma_key = "_sigma_" | append: level_lower %}
|
||||
|
||||
<div class="tab-panel {% if level == 'Research' %}active{% endif %}"
|
||||
id="tab-{{ level_lower }}" role="tabpanel">
|
||||
|
||||
{% if detection %}
|
||||
<div class="detection-meta">
|
||||
<p class="detection-goal"><strong>Goal:</strong> {{ detection.Description }}</p>
|
||||
<div class="detection-two-col">
|
||||
<div>
|
||||
<h4>Log Sources</h4>
|
||||
<ul class="log-sources">
|
||||
{% for src in detection.LogSources %}
|
||||
<li>{{ src }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
<div>
|
||||
<p><strong>FP Rate:</strong> {{ detection.ExpectedFPRate }}</p>
|
||||
<p><strong>Use Case:</strong> {{ detection.UseCase }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% if detection.Logic %}
|
||||
<h4>Detection Logic</h4>
|
||||
<pre class="logic-block"><code>{{ detection.Logic | strip }}</code></pre>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Sigma rule -->
|
||||
<div class="sigma-rule-block">
|
||||
<div class="sigma-rule-header">
|
||||
<span class="sigma-label">Sigma Rule — {{ level }} Level</span>
|
||||
<div class="sigma-actions">
|
||||
{% if detection.SigmaRule %}
|
||||
<a class="btn-sm"
|
||||
href="https://github.com/{{ site.github_username }}/{{ site.github_repo }}/blob/main/{{ detection.SigmaRule }}"
|
||||
target="_blank" rel="noopener">View on GitHub</a>
|
||||
<a class="btn-sm"
|
||||
href="https://github.com/{{ site.github_username }}/{{ site.github_repo }}/raw/main/{{ detection.SigmaRule }}"
|
||||
download>Download</a>
|
||||
{% endif %}
|
||||
<button class="btn-sm copy-btn" data-target="sigma-{{ level_lower }}">Copy</button>
|
||||
</div>
|
||||
</div>
|
||||
{% assign sigma_content = cp[sigma_key] %}
|
||||
{% if sigma_content %}
|
||||
<pre class="sigma-code" id="sigma-{{ level_lower }}"><code class="language-yaml">{{ sigma_content | xml_escape }}</code></pre>
|
||||
{% else %}
|
||||
<p class="no-sigma">Sigma rule not yet available for this level.
|
||||
<a href="{{ site.github_repo | prepend: 'https://github.com/' | append: '/' | append: site.github_username }}/{{ site.github_repo }}/blob/main/CONTRIBUTING.md"
|
||||
target="_blank" rel="noopener">Contribute one</a>.
|
||||
</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ── Evolution Timeline ─────────────────────────────────────── -->
|
||||
{% if cp.EvolutionTimeline %}
|
||||
<section class="detail-section">
|
||||
<h2>Evolution Timeline</h2>
|
||||
<p class="section-intro">New tools appear; the chokepoint stays the same.</p>
|
||||
<div class="timeline">
|
||||
{% for event in cp.EvolutionTimeline reversed %}
|
||||
<div class="timeline-item">
|
||||
<div class="timeline-date">{{ event.Date }}</div>
|
||||
<div class="timeline-content">
|
||||
<h4>{{ event.Event }}</h4>
|
||||
<p><strong>Change:</strong> {{ event.Change }}</p>
|
||||
<p><strong>Detection impact:</strong> {{ event.DetectionImpact }}</p>
|
||||
<p class="constant-line"><strong>The constant:</strong>
|
||||
<em>{{ event.TheConstant }}</em></p>
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- ── Known Bypasses ─────────────────────────────────────────── -->
|
||||
{% if cp.KnownBypasses %}
|
||||
<section class="detail-section">
|
||||
<h2>Known Bypasses & Mitigations</h2>
|
||||
<div class="table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr><th>Bypass Method</th><th>Mitigation</th><th>Detection</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for b in cp.KnownBypasses %}
|
||||
<tr>
|
||||
<td>{{ b.Bypass }}</td>
|
||||
<td>{{ b.Mitigation }}</td>
|
||||
<td>{{ b.Detection }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- ── OSINT Sources ──────────────────────────────────────────── -->
|
||||
{% if cp.OsintSources %}
|
||||
<section class="detail-section">
|
||||
<h2>OSINT Sources</h2>
|
||||
<div class="osint-grid">
|
||||
{% for src in cp.OsintSources %}
|
||||
<div class="osint-card">
|
||||
<span class="osint-platform">{{ src.Platform }}</span>
|
||||
<code class="osint-query">{{ src.Query }}</code>
|
||||
{% if src.Notes %}<p class="osint-notes">{{ src.Notes }}</p>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- ── Intel Resources ────────────────────────────────────────── -->
|
||||
{% if cp.Intel %}
|
||||
<section class="detail-section">
|
||||
<h2>Free Intel Resources</h2>
|
||||
<ul class="intel-list">
|
||||
{% for intel in cp.Intel %}
|
||||
<li>
|
||||
<a href="{{ intel.URL }}" target="_blank" rel="noopener">{{ intel.Name }}</a>
|
||||
{% if intel.Description %}— {{ intel.Description | truncate: 120 }}{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- ── Related Chokepoints ────────────────────────────────────── -->
|
||||
{% if cp.RelatedChokepoints %}
|
||||
<section class="detail-section">
|
||||
<h2>Related Chokepoints</h2>
|
||||
<div class="related-grid">
|
||||
{% for slug in cp.RelatedChokepoints %}
|
||||
{% assign related = site.data.chokepoints | where: "_slug", slug | first %}
|
||||
{% if related %}
|
||||
<a class="related-card" href="{{ '/chokepoints/' | append: slug | append: '/' | relative_url }}">
|
||||
<span class="related-name">{{ related.Name }}</span>
|
||||
<span class="badge priority-{{ related.DetectionPriority | downcase }} sm">{{ related.DetectionPriority }}</span>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
<!-- ── References ─────────────────────────────────────────────── -->
|
||||
{% if cp.References %}
|
||||
<section class="detail-section">
|
||||
<h2>References</h2>
|
||||
<ul class="ref-list">
|
||||
{% for ref in cp.References %}
|
||||
<li><a href="{{ ref }}" target="_blank" rel="noopener">{{ ref }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
{% endif %}
|
||||
|
||||
</div><!-- /container -->
|
||||
</article>
|
||||
|
||||
<script>
|
||||
/* Tab switching for sigma rules */
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
const panel = btn.dataset.tab;
|
||||
btn.closest('.sigma-tabs').querySelectorAll('.tab-btn').forEach(b => {
|
||||
b.classList.toggle('active', b === btn);
|
||||
b.setAttribute('aria-selected', b === btn ? 'true' : 'false');
|
||||
});
|
||||
btn.closest('.sigma-tabs').querySelectorAll('.tab-panel').forEach(p => {
|
||||
p.classList.toggle('active', p.id === 'tab-' + panel);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* Copy sigma rule to clipboard */
|
||||
document.querySelectorAll('.copy-btn').forEach(btn => {
|
||||
btn.addEventListener('click', async () => {
|
||||
const target = document.getElementById(btn.dataset.target);
|
||||
if (!target) return;
|
||||
try {
|
||||
await navigator.clipboard.writeText(target.textContent);
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = 'Copied!';
|
||||
setTimeout(() => { btn.textContent = orig; }, 1500);
|
||||
} catch {
|
||||
btn.textContent = 'Failed';
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
{% endif %}
|
||||
@@ -0,0 +1,37 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<meta name="description" content="{{ page.description | default: site.description | strip_html | normalize_whitespace | truncate: 160 }}" />
|
||||
<title>{% if page.title and page.title != site.title %}{{ page.title }} | {{ site.title }}{% else %}{{ site.title }}{% endif %}</title>
|
||||
{% seo %}
|
||||
<link rel="stylesheet" href="{{ '/assets/css/style.css' | relative_url }}" />
|
||||
<link rel="icon" type="image/svg+xml" href="{{ '/assets/img/favicon.svg' | relative_url }}" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
{% include nav.html %}
|
||||
|
||||
<main id="main-content">
|
||||
{{ content }}
|
||||
</main>
|
||||
|
||||
<footer class="site-footer">
|
||||
<div class="footer-inner">
|
||||
<span>
|
||||
<a href="{{ '/' | relative_url }}">Detection Chokepoints</a>
|
||||
— community detection engineering resource
|
||||
</span>
|
||||
<span class="footer-links">
|
||||
<a href="https://github.com/{{ site.github_username }}/{{ site.github_repo }}" target="_blank" rel="noopener">
|
||||
GitHub
|
||||
</a>
|
||||
<a href="{{ '/CONTRIBUTING' | relative_url }}">Contribute</a>
|
||||
<a href="https://attack.mitre.org/" target="_blank" rel="noopener">MITRE ATT&CK</a>
|
||||
</span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,714 @@
|
||||
/* ─── Reset & Base ─────────────────────────────────────────────────────────── */
|
||||
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
|
||||
:root {
|
||||
--bg: #0d1117;
|
||||
--bg-card: #161b22;
|
||||
--bg-card-hover: #1c2128;
|
||||
--bg-input: #21262d;
|
||||
--border: #30363d;
|
||||
--border-focus:#f0883e;
|
||||
--text: #e6edf3;
|
||||
--text-muted: #8b949e;
|
||||
--text-dim: #6e7681;
|
||||
--accent: #f0883e;
|
||||
--accent-dark: #c06c2e;
|
||||
--link: #58a6ff;
|
||||
|
||||
--critical: #da3633;
|
||||
--high: #e3b341;
|
||||
--medium: #3fb950;
|
||||
--low: #388bfd;
|
||||
|
||||
--font-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
--font-mono: ui-monospace, "SFMono-Regular", "SF Mono", Consolas, "Liberation Mono", Menlo, monospace;
|
||||
|
||||
--radius: 6px;
|
||||
--radius-lg: 12px;
|
||||
--nav-h: 56px;
|
||||
--max-w: 1280px;
|
||||
}
|
||||
|
||||
html { scroll-behavior: smooth; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--text);
|
||||
font-family: var(--font-sans);
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
a { color: var(--link); text-decoration: none; }
|
||||
a:hover { text-decoration: underline; }
|
||||
|
||||
/* ─── Navigation ────────────────────────────────────────────────────────────── */
|
||||
.site-nav {
|
||||
background: rgba(13,17,23,0.85);
|
||||
backdrop-filter: blur(12px);
|
||||
border-bottom: 1px solid var(--border);
|
||||
height: var(--nav-h);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
.nav-inner {
|
||||
max-width: var(--max-w);
|
||||
margin: 0 auto;
|
||||
padding: 0 1.5rem;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 2rem;
|
||||
}
|
||||
.nav-logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .5rem;
|
||||
color: var(--text);
|
||||
font-weight: 700;
|
||||
font-size: 1rem;
|
||||
text-decoration: none;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.nav-logo svg { color: var(--accent); }
|
||||
.nav-logo:hover { text-decoration: none; color: var(--accent); }
|
||||
.nav-links {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
gap: .25rem;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
}
|
||||
.nav-links a {
|
||||
color: var(--text-muted);
|
||||
padding: .35rem .75rem;
|
||||
border-radius: var(--radius);
|
||||
font-size: .875rem;
|
||||
transition: color .15s, background .15s;
|
||||
text-decoration: none;
|
||||
}
|
||||
.nav-links a:hover { color: var(--text); background: var(--bg-card); }
|
||||
.nav-github { display: flex; align-items: center; }
|
||||
.nav-github svg { display: block; }
|
||||
|
||||
/* ─── Hero ──────────────────────────────────────────────────────────────────── */
|
||||
.hero {
|
||||
background: linear-gradient(160deg, #0d1117 0%, #161b22 50%, #0d1117 100%);
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding: 4rem 1.5rem 3rem;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.hero::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(ellipse 60% 50% at 50% 0%, rgba(240,136,62,.12) 0%, transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
.hero-inner { max-width: 720px; margin: 0 auto; position: relative; }
|
||||
.hero-title {
|
||||
font-size: clamp(2rem, 5vw, 3.5rem);
|
||||
font-weight: 800;
|
||||
letter-spacing: -.02em;
|
||||
color: var(--text);
|
||||
line-height: 1.1;
|
||||
margin-bottom: .5rem;
|
||||
}
|
||||
.hero-tagline {
|
||||
font-size: clamp(1rem, 2.5vw, 1.35rem);
|
||||
color: var(--accent);
|
||||
font-weight: 600;
|
||||
font-style: italic;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
.hero-sub {
|
||||
color: var(--text-muted);
|
||||
font-size: .975rem;
|
||||
max-width: 580px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ─── Search Section ────────────────────────────────────────────────────────── */
|
||||
.search-section {
|
||||
max-width: var(--max-w);
|
||||
margin: 0 auto;
|
||||
padding: 2rem 1.5rem 0;
|
||||
}
|
||||
.search-wrap { margin-bottom: 1rem; }
|
||||
.search-box-wrap {
|
||||
position: relative;
|
||||
max-width: 640px;
|
||||
}
|
||||
.search-icon {
|
||||
position: absolute;
|
||||
left: .875rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: var(--text-dim);
|
||||
pointer-events: none;
|
||||
}
|
||||
.search-box {
|
||||
width: 100%;
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
color: var(--text);
|
||||
font-size: 1rem;
|
||||
font-family: var(--font-sans);
|
||||
padding: .75rem 3rem .75rem 2.75rem;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
.search-box::placeholder { color: var(--text-dim); }
|
||||
.search-box:focus {
|
||||
border-color: var(--border-focus);
|
||||
box-shadow: 0 0 0 3px rgba(240,136,62,.15);
|
||||
}
|
||||
.search-shortcut {
|
||||
position: absolute;
|
||||
right: .875rem;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 4px;
|
||||
color: var(--text-dim);
|
||||
font-family: var(--font-mono);
|
||||
font-size: .7rem;
|
||||
padding: .1rem .35rem;
|
||||
pointer-events: none;
|
||||
}
|
||||
.search-box:focus ~ .search-shortcut { display: none; }
|
||||
|
||||
.filter-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: .5rem;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
.filter-label {
|
||||
font-size: .75rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .06em;
|
||||
min-width: 4.5rem;
|
||||
}
|
||||
.filter-chip {
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 2rem;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-size: .8125rem;
|
||||
padding: .25rem .875rem;
|
||||
transition: background .15s, border-color .15s, color .15s;
|
||||
font-family: var(--font-sans);
|
||||
}
|
||||
.filter-chip:hover { background: var(--bg-card-hover); color: var(--text); }
|
||||
.filter-chip.active { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
|
||||
|
||||
.priority-chip.critical { border-color: var(--critical); color: var(--critical); }
|
||||
.priority-chip.critical.active { background: var(--critical); color: #fff; }
|
||||
.priority-chip.high { border-color: var(--high); color: var(--high); }
|
||||
.priority-chip.high.active { background: var(--high); color: #000; }
|
||||
.priority-chip.medium { border-color: var(--medium); color: var(--medium); }
|
||||
.priority-chip.medium.active { background: var(--medium); color: #000; }
|
||||
.priority-chip.low { border-color: var(--low); color: var(--low); }
|
||||
.priority-chip.low.active { background: var(--low); color: #fff; }
|
||||
|
||||
/* ─── Results meta ──────────────────────────────────────────────────────────── */
|
||||
.results-meta {
|
||||
max-width: var(--max-w);
|
||||
margin: .75rem auto 0;
|
||||
padding: 0 1.5rem;
|
||||
font-size: .8125rem;
|
||||
color: var(--text-dim);
|
||||
min-height: 1.4rem;
|
||||
}
|
||||
|
||||
/* ─── Grid ──────────────────────────────────────────────────────────────────── */
|
||||
.chokepoints-grid {
|
||||
max-width: var(--max-w);
|
||||
margin: 1.25rem auto 4rem;
|
||||
padding: 0 1.5rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 1rem;
|
||||
}
|
||||
@media (max-width: 1024px) { .chokepoints-grid { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (max-width: 640px) { .chokepoints-grid { grid-template-columns: 1fr; padding: 0 1rem; } }
|
||||
|
||||
/* ─── Card ──────────────────────────────────────────────────────────────────── */
|
||||
.chokepoint-card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
transition: border-color .15s, box-shadow .15s, transform .15s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
.chokepoint-card::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0; left: 0; right: 0;
|
||||
height: 3px;
|
||||
border-radius: var(--radius-lg) var(--radius-lg) 0 0;
|
||||
}
|
||||
.chokepoint-card[data-priority="CRITICAL"]::before { background: var(--critical); }
|
||||
.chokepoint-card[data-priority="HIGH"]::before { background: var(--high); }
|
||||
.chokepoint-card[data-priority="MEDIUM"]::before { background: var(--medium); }
|
||||
.chokepoint-card[data-priority="LOW"]::before { background: var(--low); }
|
||||
|
||||
.chokepoint-card:hover {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 1px var(--accent), 0 8px 24px rgba(0,0,0,.4);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.card-link {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: .6rem;
|
||||
padding: 1.25rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
height: 100%;
|
||||
}
|
||||
.card-link:hover { text-decoration: none; color: inherit; }
|
||||
|
||||
.card-header { display: flex; align-items: center; gap: .5rem; }
|
||||
.priority-badge {
|
||||
font-size: .6875rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
padding: .15rem .5rem;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.priority-badge.priority-critical { background: rgba(218,54,51,.2); color: var(--critical); border: 1px solid rgba(218,54,51,.4); }
|
||||
.priority-badge.priority-high { background: rgba(227,179,65,.2); color: var(--high); border: 1px solid rgba(227,179,65,.4); }
|
||||
.priority-badge.priority-medium { background: rgba(63,185,80,.2); color: var(--medium); border: 1px solid rgba(63,185,80,.4); }
|
||||
.priority-badge.priority-low { background: rgba(56,139,253,.2); color: var(--low); border: 1px solid rgba(56,139,253,.4); }
|
||||
|
||||
.status-badge {
|
||||
font-size: .65rem;
|
||||
font-weight: 600;
|
||||
padding: .1rem .4rem;
|
||||
border-radius: 3px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .05em;
|
||||
background: var(--bg-input);
|
||||
color: var(--text-dim);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.status-badge.status-emerging { color: var(--accent); border-color: rgba(240,136,62,.4); background: rgba(240,136,62,.1); }
|
||||
.status-badge.status-declining { color: var(--text-dim); }
|
||||
.status-badge.status-legacy { color: var(--text-dim); opacity: .7; }
|
||||
|
||||
.card-title {
|
||||
font-size: 1.05rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
line-height: 1.3;
|
||||
}
|
||||
.card-tactic {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: .35rem;
|
||||
font-size: .75rem;
|
||||
color: var(--text-dim);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .05em;
|
||||
}
|
||||
.card-description {
|
||||
font-size: .875rem;
|
||||
color: var(--text-muted);
|
||||
line-height: 1.5;
|
||||
flex-grow: 1;
|
||||
}
|
||||
.card-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: .5rem;
|
||||
margin-top: .25rem;
|
||||
}
|
||||
.card-mitre { display: flex; flex-wrap: wrap; gap: .3rem; }
|
||||
.mitre-chip {
|
||||
background: rgba(56,139,253,.12);
|
||||
border: 1px solid rgba(56,139,253,.25);
|
||||
border-radius: 3px;
|
||||
color: var(--low);
|
||||
font-family: var(--font-mono);
|
||||
font-size: .7rem;
|
||||
padding: .1rem .4rem;
|
||||
}
|
||||
.mitre-more { opacity: .7; }
|
||||
.card-variants { font-size: .75rem; color: var(--text-dim); }
|
||||
|
||||
.card-constant {
|
||||
background: rgba(240,136,62,.06);
|
||||
border: 1px solid rgba(240,136,62,.2);
|
||||
border-radius: var(--radius);
|
||||
padding: .5rem .75rem;
|
||||
margin-top: .25rem;
|
||||
}
|
||||
.constant-label {
|
||||
display: block;
|
||||
font-size: .6rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .1em;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: .2rem;
|
||||
}
|
||||
.constant-value {
|
||||
font-size: .78rem;
|
||||
color: var(--text-muted);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Hide filtered cards */
|
||||
.chokepoint-card.hidden { display: none; }
|
||||
|
||||
/* ─── No results ────────────────────────────────────────────────────────────── */
|
||||
.no-results {
|
||||
max-width: var(--max-w);
|
||||
margin: 3rem auto;
|
||||
padding: 0 1.5rem;
|
||||
text-align: center;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.no-results svg { margin: 0 auto 1rem; display: block; opacity: .4; }
|
||||
.no-results p { font-size: 1.1rem; margin-bottom: 1.25rem; }
|
||||
.btn-outline {
|
||||
background: transparent;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-family: var(--font-sans);
|
||||
font-size: .875rem;
|
||||
padding: .5rem 1.25rem;
|
||||
transition: border-color .15s, color .15s;
|
||||
}
|
||||
.btn-outline:hover { border-color: var(--accent); color: var(--accent); }
|
||||
|
||||
/* ─── Chokepoint Detail Page ────────────────────────────────────────────────── */
|
||||
.detail-wrap {
|
||||
max-width: 960px;
|
||||
margin: 0 auto;
|
||||
padding: 2.5rem 1.5rem 5rem;
|
||||
}
|
||||
.detail-header { margin-bottom: 2rem; }
|
||||
.detail-breadcrumb { font-size: .8125rem; color: var(--text-dim); margin-bottom: 1rem; }
|
||||
.detail-breadcrumb a { color: var(--text-dim); }
|
||||
.detail-breadcrumb a:hover { color: var(--accent); }
|
||||
.detail-badges { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; margin-bottom: 1rem; }
|
||||
.detail-title {
|
||||
font-size: clamp(1.5rem, 4vw, 2.25rem);
|
||||
font-weight: 800;
|
||||
line-height: 1.2;
|
||||
margin-bottom: .75rem;
|
||||
}
|
||||
.detail-description { font-size: 1.05rem; color: var(--text-muted); max-width: 720px; }
|
||||
|
||||
.section-heading {
|
||||
font-size: 1rem;
|
||||
font-weight: 700;
|
||||
color: var(--text);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .08em;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: .5rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.detail-section { margin-bottom: 2.5rem; }
|
||||
|
||||
/* Prerequisites */
|
||||
.prereq-list { list-style: none; display: flex; flex-direction: column; gap: .5rem; }
|
||||
.prereq-item {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: .6rem;
|
||||
font-size: .9375rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
.prereq-item::before {
|
||||
content: "►";
|
||||
color: var(--accent);
|
||||
font-size: .7rem;
|
||||
margin-top: .35rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
/* Constant box */
|
||||
.constant-box {
|
||||
background: rgba(240,136,62,.07);
|
||||
border: 1px solid rgba(240,136,62,.25);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1.25rem 1.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
.constant-box-label {
|
||||
font-size: .7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .12em;
|
||||
color: var(--accent);
|
||||
text-transform: uppercase;
|
||||
margin-bottom: .4rem;
|
||||
}
|
||||
.constant-box-value {
|
||||
font-size: 1rem;
|
||||
color: var(--text);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Variations table */
|
||||
.variations-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: .875rem;
|
||||
}
|
||||
.variations-table th {
|
||||
background: var(--bg-input);
|
||||
color: var(--text-dim);
|
||||
font-size: .7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
padding: .6rem .875rem;
|
||||
text-align: left;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.variations-table td {
|
||||
padding: .75rem .875rem;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
vertical-align: top;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.variations-table tr:nth-child(odd) td { background: rgba(22,27,34,.6); }
|
||||
.variation-name { color: var(--text); font-weight: 600; }
|
||||
.prevalence-badge {
|
||||
display: inline-block;
|
||||
font-size: .7rem;
|
||||
font-weight: 600;
|
||||
padding: .1rem .4rem;
|
||||
border-radius: 3px;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
.prevalence-high { background: rgba(218,54,51,.15); color: var(--critical); }
|
||||
.prevalence-medium { background: rgba(227,179,65,.15); color: var(--high); }
|
||||
.prevalence-low { background: rgba(63,185,80,.15); color: var(--medium); }
|
||||
|
||||
/* Detection tabs */
|
||||
.detection-tabs { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 0; }
|
||||
.tab-btn {
|
||||
background: transparent;
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
font-family: var(--font-sans);
|
||||
font-size: .875rem;
|
||||
font-weight: 500;
|
||||
padding: .625rem 1.25rem;
|
||||
margin-bottom: -1px;
|
||||
transition: color .15s, border-color .15s;
|
||||
}
|
||||
.tab-btn:hover { color: var(--text); }
|
||||
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); font-weight: 700; }
|
||||
|
||||
.tab-panel { display: none; padding: 1.5rem; background: var(--bg-card); border: 1px solid var(--border); border-top: none; border-radius: 0 0 var(--radius-lg) var(--radius-lg); }
|
||||
.tab-panel.active { display: block; }
|
||||
|
||||
.logsource-list { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: 1rem; }
|
||||
.logsource-chip {
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 3px;
|
||||
color: var(--text-muted);
|
||||
font-size: .75rem;
|
||||
padding: .2rem .55rem;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
|
||||
pre.sigma-block {
|
||||
background: #010409;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: #e6edf3;
|
||||
font-family: var(--font-mono);
|
||||
font-size: .8rem;
|
||||
line-height: 1.6;
|
||||
overflow-x: auto;
|
||||
padding: 1rem 1.25rem;
|
||||
position: relative;
|
||||
margin: 0;
|
||||
white-space: pre;
|
||||
}
|
||||
.copy-btn {
|
||||
position: absolute;
|
||||
top: .6rem;
|
||||
right: .6rem;
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
color: var(--text-dim);
|
||||
cursor: pointer;
|
||||
font-family: var(--font-sans);
|
||||
font-size: .72rem;
|
||||
padding: .25rem .6rem;
|
||||
transition: color .15s, border-color .15s;
|
||||
}
|
||||
.copy-btn:hover { color: var(--accent); border-color: var(--accent); }
|
||||
.copy-btn.copied { color: var(--medium); border-color: var(--medium); }
|
||||
|
||||
/* MITRE badge (detail) */
|
||||
.mitre-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: rgba(56,139,253,.1);
|
||||
border: 1px solid rgba(56,139,253,.3);
|
||||
border-radius: var(--radius);
|
||||
color: var(--low);
|
||||
font-family: var(--font-mono);
|
||||
font-size: .8rem;
|
||||
padding: .25rem .7rem;
|
||||
text-decoration: none;
|
||||
transition: background .15s;
|
||||
}
|
||||
.mitre-badge:hover { background: rgba(56,139,253,.2); text-decoration: none; }
|
||||
|
||||
/* Timeline */
|
||||
.timeline { display: flex; flex-direction: column; gap: 1rem; }
|
||||
.timeline-item { display: flex; gap: 1rem; align-items: flex-start; }
|
||||
.timeline-date {
|
||||
font-family: var(--font-mono);
|
||||
font-size: .8rem;
|
||||
color: var(--accent);
|
||||
min-width: 6rem;
|
||||
padding-top: .1rem;
|
||||
}
|
||||
.timeline-content { color: var(--text-muted); font-size: .9rem; }
|
||||
.timeline-tag {
|
||||
display: inline-block;
|
||||
font-size: .65rem;
|
||||
font-weight: 700;
|
||||
padding: .1rem .4rem;
|
||||
border-radius: 3px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: .05em;
|
||||
margin-left: .4rem;
|
||||
}
|
||||
.timeline-tag.new { background: rgba(63,185,80,.15); color: var(--medium); }
|
||||
.timeline-tag.update { background: rgba(56,139,253,.15); color: var(--low); }
|
||||
.timeline-tag.deprecated { background: rgba(110,118,129,.15); color: var(--text-dim); }
|
||||
|
||||
/* Bypass table */
|
||||
.bypass-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: .875rem;
|
||||
}
|
||||
.bypass-table th {
|
||||
background: var(--bg-input);
|
||||
color: var(--text-dim);
|
||||
font-size: .7rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: .06em;
|
||||
text-transform: uppercase;
|
||||
padding: .6rem .875rem;
|
||||
text-align: left;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.bypass-table td {
|
||||
padding: .7rem .875rem;
|
||||
border: 1px solid var(--border);
|
||||
color: var(--text-muted);
|
||||
vertical-align: top;
|
||||
}
|
||||
.bypass-table tr:nth-child(odd) td { background: rgba(22,27,34,.6); }
|
||||
|
||||
/* OSINT grid */
|
||||
.osint-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: .75rem;
|
||||
}
|
||||
.osint-card {
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
padding: 1rem 1.25rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: border-color .15s, box-shadow .15s;
|
||||
display: block;
|
||||
}
|
||||
.osint-card:hover { border-color: var(--accent); text-decoration: none; box-shadow: 0 4px 16px rgba(0,0,0,.3); }
|
||||
.osint-name { font-weight: 700; color: var(--text); margin-bottom: .25rem; }
|
||||
.osint-type { font-size: .72rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: .06em; margin-bottom: .4rem; }
|
||||
.osint-desc { font-size: .8125rem; color: var(--text-muted); }
|
||||
|
||||
/* Intel resources */
|
||||
.intel-list { list-style: none; display: flex; flex-direction: column; gap: .5rem; }
|
||||
.intel-item { display: flex; align-items: flex-start; gap: .6rem; }
|
||||
.intel-item a { color: var(--link); font-size: .9rem; }
|
||||
.intel-item a:hover { color: var(--accent); }
|
||||
|
||||
/* Related cards */
|
||||
.related-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: .75rem; }
|
||||
.related-card {
|
||||
background: var(--bg-input);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: .875rem 1rem;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
transition: border-color .15s;
|
||||
display: block;
|
||||
}
|
||||
.related-card:hover { border-color: var(--accent); text-decoration: none; }
|
||||
.related-card-title { font-weight: 700; color: var(--text); font-size: .9rem; margin-bottom: .25rem; }
|
||||
.related-card-tactic { font-size: .75rem; color: var(--text-dim); }
|
||||
|
||||
/* References */
|
||||
.refs-list { list-style: none; display: flex; flex-direction: column; gap: .4rem; }
|
||||
.refs-list li::before { content: "→ "; color: var(--accent); }
|
||||
.refs-list a { color: var(--link); font-size: .875rem; word-break: break-all; }
|
||||
.refs-list a:hover { color: var(--accent); }
|
||||
|
||||
/* ─── Footer ────────────────────────────────────────────────────────────────── */
|
||||
.site-footer {
|
||||
background: var(--bg-card);
|
||||
border-top: 1px solid var(--border);
|
||||
margin-top: auto;
|
||||
padding: 2rem 1.5rem;
|
||||
}
|
||||
.footer-inner {
|
||||
max-width: var(--max-w);
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
font-size: .8125rem;
|
||||
color: var(--text-dim);
|
||||
}
|
||||
.footer-links { display: flex; flex-wrap: wrap; gap: 1.25rem; }
|
||||
.footer-links a { color: var(--text-dim); text-decoration: none; }
|
||||
.footer-links a:hover { color: var(--accent); }
|
||||
|
||||
/* ─── Utilities ─────────────────────────────────────────────────────────────── */
|
||||
.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0,0,0,0); white-space: nowrap; border: 0; }
|
||||
@@ -0,0 +1,6 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" fill="none">
|
||||
<path d="M16 2 L28 7 L28 17 C28 23.5 22.5 28.5 16 30 C9.5 28.5 4 23.5 4 17 L4 7 Z" fill="#161b22" stroke="#f0883e" stroke-width="1.5"/>
|
||||
<path d="M16 7 L22 10 L22 17 C22 20.5 19.5 23 16 24 C12.5 23 10 20.5 10 17 L10 10 Z" fill="#f0883e" opacity="0.15"/>
|
||||
<line x1="11" y1="16" x2="21" y2="16" stroke="#f0883e" stroke-width="2" stroke-linecap="round"/>
|
||||
<line x1="16" y1="11" x2="16" y2="21" stroke="#f0883e" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 535 B |
Vendored
+9
File diff suppressed because one or more lines are too long
@@ -0,0 +1,190 @@
|
||||
/* Detection Chokepoints — client-side search & filter
|
||||
* Depends on: fuse.min.js loaded before this script
|
||||
* Data: window.CHOKEPOINTS_DATA (injected by Jekyll from site.data.chokepoints)
|
||||
*/
|
||||
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
// ── DOM refs ──────────────────────────────────────────────────────────────
|
||||
const searchInput = document.getElementById('search-input');
|
||||
const grid = document.getElementById('chokepoints-grid');
|
||||
const noResults = document.getElementById('no-results');
|
||||
const clearBtn = document.getElementById('clear-search');
|
||||
const resultCount = document.getElementById('results-count');
|
||||
const tacticChips = document.querySelectorAll('[data-filter="tactic"]');
|
||||
const priorityChips= document.querySelectorAll('[data-filter="priority"]');
|
||||
const cards = Array.from(grid ? grid.querySelectorAll('.chokepoint-card') : []);
|
||||
|
||||
if (!grid) return; // not on index page
|
||||
|
||||
// ── State ─────────────────────────────────────────────────────────────────
|
||||
let activeTactic = 'all';
|
||||
let activePriority = 'all';
|
||||
let fuseResults = null; // null = no text query active
|
||||
|
||||
// ── Fuse.js setup ─────────────────────────────────────────────────────────
|
||||
const data = window.CHOKEPOINTS_DATA || [];
|
||||
const fuse = new Fuse(data, {
|
||||
threshold: 0.35,
|
||||
distance: 200,
|
||||
minMatchCharLength: 2,
|
||||
keys: [
|
||||
{ name: 'Name', weight: 3 },
|
||||
{ name: 'MitreIds', weight: 2.5 },
|
||||
{ name: 'Description', weight: 2 },
|
||||
{ name: 'TheConstant', weight: 1.5 },
|
||||
{ name: 'Tactic', weight: 1.5 },
|
||||
{ name: 'Prerequisites',weight: 1 },
|
||||
{ name: 'Variations.Name', weight: 1 },
|
||||
{ name: 'Variations.Description',weight: 0.8 },
|
||||
],
|
||||
});
|
||||
|
||||
// Slug → card map for fast lookups
|
||||
const slugToCard = {};
|
||||
cards.forEach(card => {
|
||||
// Cards have data-* but we need the slug; extract from the card link
|
||||
const link = card.querySelector('.card-link');
|
||||
if (!link) return;
|
||||
const href = link.getAttribute('href') || '';
|
||||
const parts = href.replace(/\/+$/, '').split('/');
|
||||
const slug = parts[parts.length - 1];
|
||||
if (slug) slugToCard[slug] = card;
|
||||
});
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
function updateCount(visible) {
|
||||
const total = cards.length;
|
||||
if (visible === total) {
|
||||
resultCount.textContent = `${total} chokepoint${total !== 1 ? 's' : ''}`;
|
||||
} else {
|
||||
resultCount.textContent = `${visible} of ${total} chokepoint${total !== 1 ? 's' : ''}`;
|
||||
}
|
||||
}
|
||||
|
||||
function normalise(str) {
|
||||
return (str || '').toLowerCase().replace(/\s+/g, '-');
|
||||
}
|
||||
|
||||
function applyFilters() {
|
||||
// Build allowed slug set from fuse results (if search active)
|
||||
let allowedSlugs = null;
|
||||
if (fuseResults !== null) {
|
||||
allowedSlugs = new Set(fuseResults.map(r => r.item._slug));
|
||||
}
|
||||
|
||||
let visible = 0;
|
||||
cards.forEach(card => {
|
||||
const tactic = normalise(card.dataset.tactic || '');
|
||||
const priority = (card.dataset.priority || '').toUpperCase();
|
||||
|
||||
// Get slug from card link
|
||||
const link = card.querySelector('.card-link');
|
||||
const href = link ? link.getAttribute('href') || '' : '';
|
||||
const parts = href.replace(/\/+$/, '').split('/');
|
||||
const slug = parts[parts.length - 1];
|
||||
|
||||
const passesSearch = allowedSlugs === null || allowedSlugs.has(slug);
|
||||
const passesTactic = activeTactic === 'all' || tactic === activeTactic;
|
||||
const passesPriority = activePriority === 'all' || priority === activePriority;
|
||||
|
||||
const show = passesSearch && passesTactic && passesPriority;
|
||||
card.classList.toggle('hidden', !show);
|
||||
if (show) visible++;
|
||||
});
|
||||
|
||||
const hasResults = visible > 0;
|
||||
noResults.hidden = hasResults;
|
||||
updateCount(visible);
|
||||
|
||||
// Sync URL query string
|
||||
const params = new URLSearchParams();
|
||||
if (searchInput.value) params.set('q', searchInput.value);
|
||||
if (activeTactic !== 'all') params.set('tactic', activeTactic);
|
||||
if (activePriority !== 'all') params.set('priority', activePriority);
|
||||
const qs = params.toString();
|
||||
const newUrl = qs ? `${location.pathname}?${qs}` : location.pathname;
|
||||
history.replaceState(null, '', newUrl);
|
||||
}
|
||||
|
||||
// ── Search ────────────────────────────────────────────────────────────────
|
||||
let debounceTimer;
|
||||
searchInput.addEventListener('input', () => {
|
||||
clearTimeout(debounceTimer);
|
||||
debounceTimer = setTimeout(() => {
|
||||
const q = searchInput.value.trim();
|
||||
fuseResults = q.length >= 2 ? fuse.search(q) : null;
|
||||
applyFilters();
|
||||
}, 150);
|
||||
});
|
||||
|
||||
// Keyboard shortcut: press "/" to focus search (unless already in an input)
|
||||
document.addEventListener('keydown', e => {
|
||||
if (e.key === '/' && document.activeElement.tagName !== 'INPUT' && document.activeElement.tagName !== 'TEXTAREA') {
|
||||
e.preventDefault();
|
||||
searchInput.focus();
|
||||
searchInput.select();
|
||||
}
|
||||
if (e.key === 'Escape' && document.activeElement === searchInput) {
|
||||
searchInput.blur();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Filter chips ──────────────────────────────────────────────────────────
|
||||
function setActiveChip(chips, value) {
|
||||
chips.forEach(chip => {
|
||||
const isActive = chip.dataset.value === value;
|
||||
chip.classList.toggle('active', isActive);
|
||||
chip.setAttribute('aria-pressed', isActive ? 'true' : 'false');
|
||||
});
|
||||
}
|
||||
|
||||
tacticChips.forEach(chip => {
|
||||
chip.addEventListener('click', () => {
|
||||
activeTactic = chip.dataset.value;
|
||||
setActiveChip(tacticChips, activeTactic);
|
||||
applyFilters();
|
||||
});
|
||||
});
|
||||
|
||||
priorityChips.forEach(chip => {
|
||||
chip.addEventListener('click', () => {
|
||||
activePriority = chip.dataset.value;
|
||||
setActiveChip(priorityChips, activePriority);
|
||||
applyFilters();
|
||||
});
|
||||
});
|
||||
|
||||
// Clear button (no-results state)
|
||||
if (clearBtn) {
|
||||
clearBtn.addEventListener('click', () => {
|
||||
searchInput.value = '';
|
||||
fuseResults = null;
|
||||
activeTactic = 'all';
|
||||
activePriority = 'all';
|
||||
setActiveChip(tacticChips, 'all');
|
||||
setActiveChip(priorityChips, 'all');
|
||||
applyFilters();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Restore state from URL ────────────────────────────────────────────────
|
||||
(function restoreFromUrl() {
|
||||
const params = new URLSearchParams(location.search);
|
||||
const q = params.get('q') || '';
|
||||
const tactic = params.get('tactic') || 'all';
|
||||
const priority = params.get('priority') || 'all';
|
||||
|
||||
if (q) {
|
||||
searchInput.value = q;
|
||||
fuseResults = q.length >= 2 ? fuse.search(q) : null;
|
||||
}
|
||||
activeTactic = tactic;
|
||||
activePriority = priority;
|
||||
setActiveChip(tacticChips, tactic);
|
||||
setActiveChip(priorityChips, priority);
|
||||
applyFilters();
|
||||
})();
|
||||
|
||||
})();
|
||||
@@ -147,7 +147,7 @@ KnownBypasses:
|
||||
Detection: Detect kernel driver loading (BYOVD) before service manipulation
|
||||
- Bypass: Booting to Safe Mode (security services don't start)
|
||||
Mitigation: N/A — services genuinely not running in Safe Mode
|
||||
Detection: Detect Safe Mode boot on servers (registry: HKLM\SYSTEM\CurrentControlSet\Control\SafeBoot)
|
||||
Detection: "Detect Safe Mode boot on servers (registry: HKLM\\SYSTEM\\CurrentControlSet\\Control\\SafeBoot)"
|
||||
- Bypass: Renaming service before stopping (evades name-based filters)
|
||||
Mitigation: Allowlist by service binary path, not service name
|
||||
Detection: Monitor service configuration changes (sc config); service rename before stop is itself suspicious
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
---
|
||||
layout: default
|
||||
title: Detection Chokepoints
|
||||
description: "Community resource for detection engineering: high-signal chokepoints that every attacker must pass through."
|
||||
---
|
||||
|
||||
<section class="hero">
|
||||
<div class="hero-inner">
|
||||
<h1 class="hero-title">Detection Chokepoints</h1>
|
||||
<p class="hero-tagline">TTPs evolve. Chokepoints don't.</p>
|
||||
<p class="hero-sub">Attack prerequisites that cannot be bypassed regardless of tool choice — high-signal, low-volume detection opportunities for every defender.</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="search-section" aria-label="Search and filter">
|
||||
<div class="search-wrap">
|
||||
<div class="search-box-wrap">
|
||||
<svg class="search-icon" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true">
|
||||
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
||||
</svg>
|
||||
<input
|
||||
type="search"
|
||||
id="search-input"
|
||||
class="search-box"
|
||||
placeholder="Search chokepoints, techniques, MITRE IDs…"
|
||||
aria-label="Search chokepoints"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
/>
|
||||
<kbd class="search-shortcut" aria-hidden="true">/</kbd>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-row" role="group" aria-label="Filter by tactic">
|
||||
<span class="filter-label">Tactic</span>
|
||||
<button class="filter-chip active" data-filter="tactic" data-value="all">All</button>
|
||||
<button class="filter-chip" data-filter="tactic" data-value="initial-access">Initial Access</button>
|
||||
<button class="filter-chip" data-filter="tactic" data-value="lateral-movement">Lateral Movement</button>
|
||||
<button class="filter-chip" data-filter="tactic" data-value="defense-evasion">Defense Evasion</button>
|
||||
<button class="filter-chip" data-filter="tactic" data-value="execution">Execution</button>
|
||||
<button class="filter-chip" data-filter="tactic" data-value="persistence">Persistence</button>
|
||||
</div>
|
||||
|
||||
<div class="filter-row" role="group" aria-label="Filter by priority">
|
||||
<span class="filter-label">Priority</span>
|
||||
<button class="filter-chip active" data-filter="priority" data-value="all">All</button>
|
||||
<button class="filter-chip priority-chip critical" data-filter="priority" data-value="CRITICAL">Critical</button>
|
||||
<button class="filter-chip priority-chip high" data-filter="priority" data-value="HIGH">High</button>
|
||||
<button class="filter-chip priority-chip medium" data-filter="priority" data-value="MEDIUM">Medium</button>
|
||||
<button class="filter-chip priority-chip low" data-filter="priority" data-value="LOW">Low</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="results-meta" aria-live="polite" aria-atomic="true">
|
||||
<span id="results-count"></span>
|
||||
</section>
|
||||
|
||||
<main id="chokepoints-grid" class="chokepoints-grid" aria-label="Chokepoints">
|
||||
{% assign sorted = site.data.chokepoints | sort: "DetectionPriority" %}
|
||||
{% for cp in sorted %}
|
||||
{% include chokepoint-card.html cp=cp %}
|
||||
{% endfor %}
|
||||
</main>
|
||||
|
||||
<div id="no-results" class="no-results" hidden>
|
||||
<svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" aria-hidden="true">
|
||||
<circle cx="11" cy="11" r="8"/><path d="m21 21-4.35-4.35"/>
|
||||
<path d="M8 11h6M11 8v6" transform="rotate(45 11 11)"/>
|
||||
</svg>
|
||||
<p>No chokepoints match your search.</p>
|
||||
<button id="clear-search" class="btn-outline">Clear filters</button>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// Pass Jekyll data to JS
|
||||
window.CHOKEPOINTS_DATA = {{ site.data.chokepoints | jsonify }};
|
||||
window.SITE_BASEURL = "{{ site.baseurl }}";
|
||||
</script>
|
||||
<script src="{{ '/assets/js/fuse.min.js' | relative_url }}"></script>
|
||||
<script src="{{ '/assets/js/search.js' | relative_url }}"></script>
|
||||
@@ -0,0 +1,158 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Pre-build script for the Detection Chokepoints Jekyll site.
|
||||
|
||||
Reads YAML entries from chokepoints/<tactic>/*.yml, reads matching sigma rule
|
||||
files from sigma-rules/<dir>/{research,hunt,analyst}.yml, and generates:
|
||||
|
||||
- _data/chokepoints.yml (Jekyll data layer for Liquid templates)
|
||||
- assets/js/search-index.json (Fuse.js client-side search index)
|
||||
- _chokepoints/<slug>.md (Jekyll collection stubs, one per chokepoint)
|
||||
|
||||
Run before `jekyll build`. The GitHub Actions workflow does this automatically.
|
||||
"""
|
||||
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
import yaml
|
||||
|
||||
REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
CHOKEPOINTS_GLOB = os.path.join(REPO_ROOT, "chokepoints", "*", "*.yml")
|
||||
SIGMA_RULES_DIR = os.path.join(REPO_ROOT, "sigma-rules")
|
||||
DATA_DIR = os.path.join(REPO_ROOT, "_data")
|
||||
COLLECTION_DIR = os.path.join(REPO_ROOT, "_chokepoints")
|
||||
ASSETS_JS_DIR = os.path.join(REPO_ROOT, "assets", "js")
|
||||
SIGMA_LEVELS = ("research", "hunt", "analyst")
|
||||
|
||||
|
||||
def extract_sigma_dir(detections):
|
||||
"""Derive the sigma-rules sub-directory from the first SigmaRule path.
|
||||
|
||||
e.g. "sigma-rules/clickfix/research.yml" -> "clickfix"
|
||||
"""
|
||||
for det in detections or []:
|
||||
rule_path = det.get("SigmaRule", "")
|
||||
if rule_path:
|
||||
parts = rule_path.replace("\\", "/").split("/")
|
||||
if len(parts) >= 2:
|
||||
return parts[1]
|
||||
return None
|
||||
|
||||
|
||||
def read_sigma_rules(sigma_dir):
|
||||
"""Return a dict mapping level -> raw YAML text (or None if file missing)."""
|
||||
rules = {}
|
||||
if not sigma_dir:
|
||||
return rules
|
||||
for level in SIGMA_LEVELS:
|
||||
path = os.path.join(SIGMA_RULES_DIR, sigma_dir, f"{level}.yml")
|
||||
if os.path.exists(path):
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
rules[level] = fh.read()
|
||||
else:
|
||||
rules[level] = None
|
||||
return rules
|
||||
|
||||
|
||||
def load_chokepoints():
|
||||
"""Load, enrich, and return all chokepoint entries as a list of dicts."""
|
||||
entries = []
|
||||
for path in sorted(glob.glob(CHOKEPOINTS_GLOB)):
|
||||
tactic = os.path.basename(os.path.dirname(path))
|
||||
slug = os.path.splitext(os.path.basename(path))[0]
|
||||
|
||||
with open(path, "r", encoding="utf-8") as fh:
|
||||
data = yaml.safe_load(fh)
|
||||
|
||||
if not data or not isinstance(data, dict):
|
||||
print(f"Warning: skipping empty/invalid YAML at {path}", file=sys.stderr)
|
||||
continue
|
||||
|
||||
# Computed fields (prefixed with _ so contributors know they're generated)
|
||||
data["_tactic"] = tactic
|
||||
data["_slug"] = slug
|
||||
data["_source_path"] = os.path.relpath(path, REPO_ROOT)
|
||||
|
||||
sigma_dir = extract_sigma_dir(data.get("Detections", []))
|
||||
data["_sigma_dir"] = sigma_dir
|
||||
sigma_rules = read_sigma_rules(sigma_dir)
|
||||
for level in SIGMA_LEVELS:
|
||||
data[f"_sigma_{level}"] = sigma_rules.get(level)
|
||||
|
||||
# Flatten text fields for search index
|
||||
prereqs = data.get("Prerequisites", []) or []
|
||||
data["_prerequisites_text"] = " ".join(str(p) for p in prereqs)
|
||||
|
||||
variations = data.get("Variations", []) or []
|
||||
data["_variation_names"] = " ".join(
|
||||
str(v.get("Name", "")) for v in variations if isinstance(v, dict)
|
||||
)
|
||||
|
||||
entries.append(data)
|
||||
|
||||
return entries
|
||||
|
||||
|
||||
def write_data_file(entries):
|
||||
"""Write _data/chokepoints.yml for Jekyll Liquid templates."""
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
out_path = os.path.join(DATA_DIR, "chokepoints.yml")
|
||||
with open(out_path, "w", encoding="utf-8") as fh:
|
||||
yaml.dump(entries, fh, default_flow_style=False, allow_unicode=True,
|
||||
sort_keys=False)
|
||||
print(f" Wrote {os.path.relpath(out_path, REPO_ROOT)} ({len(entries)} entries)")
|
||||
|
||||
|
||||
def write_search_index(entries):
|
||||
"""Write assets/js/search-index.json as a lean JSON array for Fuse.js."""
|
||||
os.makedirs(ASSETS_JS_DIR, exist_ok=True)
|
||||
index = []
|
||||
for e in entries:
|
||||
index.append({
|
||||
"id": e.get("Id", ""),
|
||||
"name": e.get("Name", ""),
|
||||
"slug": e["_slug"],
|
||||
"tactic": e["_tactic"],
|
||||
"tactics": e.get("Tactics", []),
|
||||
"mitreIds": e.get("MitreIds", []),
|
||||
"detectionPriority": e.get("DetectionPriority", ""),
|
||||
"threatPrevalence": e.get("ThreatPrevalence", ""),
|
||||
"detectionDifficulty": e.get("DetectionDifficulty", ""),
|
||||
"description": (e.get("Description") or "").strip(),
|
||||
"prerequisites": e.get("_prerequisites_text", ""),
|
||||
"variationNames": e.get("_variation_names", ""),
|
||||
})
|
||||
out_path = os.path.join(ASSETS_JS_DIR, "search-index.json")
|
||||
with open(out_path, "w", encoding="utf-8") as fh:
|
||||
json.dump(index, fh, indent=2)
|
||||
print(f" Wrote {os.path.relpath(out_path, REPO_ROOT)} ({len(index)} entries)")
|
||||
|
||||
|
||||
def write_collection_stubs(entries):
|
||||
"""Write _chokepoints/<slug>.md — thin Jekyll collection stubs."""
|
||||
os.makedirs(COLLECTION_DIR, exist_ok=True)
|
||||
for e in entries:
|
||||
stub_path = os.path.join(COLLECTION_DIR, f"{e['_slug']}.md")
|
||||
front = {
|
||||
"layout": "chokepoint",
|
||||
"slug": e["_slug"],
|
||||
"title": e.get("Name", e["_slug"]),
|
||||
}
|
||||
content = "---\n" + yaml.dump(front, default_flow_style=False) + "---\n"
|
||||
with open(stub_path, "w", encoding="utf-8") as fh:
|
||||
fh.write(content)
|
||||
print(f" Wrote {len(entries)} stub files to _chokepoints/")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
print("Aggregating chokepoint data...")
|
||||
entries = load_chokepoints()
|
||||
print(f" Loaded {len(entries)} chokepoints")
|
||||
write_data_file(entries)
|
||||
write_search_index(entries)
|
||||
write_collection_stubs(entries)
|
||||
print("Done.")
|
||||
Reference in New Issue
Block a user