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:
Claude
2026-03-01 03:50:22 +00:00
parent 2ab809fa61
commit 07e9c83e24
15 changed files with 1726 additions and 1 deletions
+65
View File
@@ -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
View File
@@ -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/
+7
View File
@@ -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
View File
@@ -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/"
+39
View File
@@ -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>
+41
View File
@@ -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>
+319
View File
@@ -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">&larr; 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 }} &middot; Updated {{ cp.LastUpdated }}
&middot; <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">&#10003;</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 &mdash; {{ 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 &amp; 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 %}&mdash; {{ 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 %}
+37
View File
@@ -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>
&mdash; 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&amp;CK</a>
</span>
</div>
</footer>
</body>
</html>
+714
View File
@@ -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; }
+6
View File
@@ -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

+9
View File
File diff suppressed because one or more lines are too long
+190
View File
@@ -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
View File
@@ -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>
+158
View File
@@ -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.")