diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index c541a2b8..56707972 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -4,18 +4,18 @@ "name": "borghei", "url": "https://borghei.me" }, - "description": "Production-ready AI skill packages for every coding assistant — 372 expert skills across 20 domains: engineering, project management, marketing, C-level advisory, regulatory compliance, business growth, business operations, legal, product, personal productivity, vertical advisors, data analytics, sales, documents, HR, research, research ops, finance, markdown-html publishing, and workflow meta-skills", + "description": "Production-ready AI skill packages for every coding assistant — 385 expert skills across 21 domains: engineering, project management, marketing, C-level advisory, regulatory compliance, business growth, business operations, legal, product, personal productivity, vertical advisors, data analytics, sales, documents, HR, research, research ops, finance, markdown-html publishing, and workflow meta-skills", "homepage": "https://github.com/borghei/Claude-Skills", "repository": "https://github.com/borghei/Claude-Skills", "metadata": { - "description": "372 production-ready AI skill packages across 20 domains, installable per-domain as Claude Code plugins", + "description": "385 production-ready AI skill packages across 21 domains, installable per-domain as Claude Code plugins", "version": "3.0.0" }, "plugins": [ { "name": "engineering-advanced-skills", "source": "./engineering", - "description": "82 engineering skills: architecture, fullstack, DevOps, security, AI/ML, data engineering, and agent tooling", + "description": "92 engineering skills: architecture, fullstack, DevOps, security, AI/ML, data engineering, and agent tooling", "version": "1.0.0", "author": { "name": "borghei" @@ -375,6 +375,24 @@ "publishing" ], "category": "documentation" + }, + { + "name": "tools-skills", + "source": "./tools", + "description": "12 platform toolkit skills: the LinkedIn suite — post writing, humanizing, hooks, comments, replies, thread tracking, planning, repurposing, story interviews, profile, employee advocacy, and engagement analytics", + "version": "1.0.0", + "author": { + "name": "borghei" + }, + "keywords": [ + "linkedin", + "social", + "content", + "personal-brand", + "employee-advocacy", + "analytics" + ], + "category": "marketing" } ] } diff --git a/.gemini/skills-index.json b/.gemini/skills-index.json index 7cb8522a..609b69cf 100644 --- a/.gemini/skills-index.json +++ b/.gemini/skills-index.json @@ -1,7 +1,7 @@ { "version": "1.0.0", - "updated": "2026-09-27T21:13:15+00:00", - "total_skills": 372, + "updated": "2026-10-07T00:46:25+00:00", + "total_skills": 385, "skills": [ { "name": "a11y-audit", @@ -973,6 +973,27 @@ "security" ] }, + { + "name": "security-audit", + "description": "Multi-agent source security audit with a coverage ledger, independently verified findings, and machine-checked records. Use when asked to audit a codebase, find vulnerabilities, or run a source-level pen test.", + "domain": "engineering", + "path": "engineering/security-audit/SKILL.md", + "tools": [ + "audit_common.py", + "audit_run.py", + "ledger_rules.py", + "promote_evidence.py", + "validate_coverage_ledger.py", + "validate_findings.py" + ], + "tags": [ + "security-audit", + "vulnerability-research", + "multi-agent", + "code-review", + "appsec" + ] + }, { "name": "self-improving-agent", "description": "Patterns for AI agents that learn from their own execution, detect failure modes, and improve autonomously. Use when building agents that get better over time, managing auto- memory, or designing self-correcting feedback loops.", @@ -5528,6 +5549,214 @@ "accessibility" ] }, + { + "name": "linkedin-comment-writer", + "description": "Drafts comments on other people's LinkedIn posts that add a specific the post lacked, then gates them offline. Use when commenting on a post, engaging with a prospect or peer, or checking a comment before pasting it.", + "domain": "tools", + "path": "tools/linkedin/linkedin-comment-writer/SKILL.md", + "tools": [ + "comment_linter.py", + "comment_rules.py" + ], + "tags": [ + "linkedin", + "comments", + "engagement", + "social-selling", + "writing" + ] + }, + { + "name": "linkedin-content-planner", + "description": "Builds a weekly or monthly LinkedIn publishing plan from content pillars, a posting cadence and a format mix, with a founder-oriented pillar set as an alternative to the general one. Use when planning a week, a month or a launch period of posts rather than drafting a single post.", + "domain": "tools", + "path": "tools/linkedin/linkedin-content-planner/SKILL.md", + "tools": [ + "plan_builder.py", + "plan_rules.py" + ], + "tags": [ + "linkedin", + "content-calendar", + "content-pillars", + "editorial-planning", + "founder-content" + ] + }, + { + "name": "linkedin-content-repurposer", + "description": "Turns something made for another channel (a thread, a video or talk transcript, a blog post, a newsletter) into a post that reads as native to LinkedIn, without changing what the source says. Use when existing material should become a LinkedIn post rather than writing one from a blank page.", + "domain": "tools", + "path": "tools/linkedin/linkedin-content-repurposer/SKILL.md", + "tools": [ + "draft_fidelity_check.py", + "repurpose_analyzer.py", + "repurpose_rules.py" + ], + "tags": [ + "linkedin", + "repurposing", + "content-adaptation", + "transcripts", + "fidelity-check" + ] + }, + { + "name": "linkedin-employee-advocacy", + "description": "Designs and runs an employee advocacy programme: who posts what and how often, review rules, and what is never scripted. Use when getting a team posting, writing an advocacy policy, or fixing a programme that has stalled.", + "domain": "tools", + "path": "tools/linkedin/linkedin-employee-advocacy/SKILL.md", + "tools": [ + "advocacy_planner.py", + "advocacy_rules.py" + ], + "tags": [ + "linkedin", + "employee-advocacy", + "governance", + "team-cadence", + "social-policy" + ] + }, + { + "name": "linkedin-engagement-analytics", + "description": "Segments who reacted to and commented on a post from a CSV or JSON export, and says whether it reached the intended audience. Use when reviewing a post's engagers, checking audience fit, or building a baseline.", + "domain": "tools", + "path": "tools/linkedin/linkedin-engagement-analytics/SKILL.md", + "tools": [ + "engager_segmenter.py", + "segment_rules.py" + ], + "tags": [ + "linkedin", + "analytics", + "audience-segmentation", + "engagement", + "privacy" + ] + }, + { + "name": "linkedin-hook-analyzer", + "description": "Extracts and classifies the opening-line pattern of LinkedIn posts the user pastes or saves, and turns each into a reusable slot template with cautions. Use when studying why an opening works, building a swipe file, or adapting a hook pattern without copying it.", + "domain": "tools", + "path": "tools/linkedin/linkedin-hook-analyzer/SKILL.md", + "tools": [ + "hook_classifier.py", + "hook_patterns.py" + ], + "tags": [ + "linkedin", + "hooks", + "swipe-file", + "pattern-analysis", + "copywriting" + ] + }, + { + "name": "linkedin-humanizer", + "description": "Audits and rewrites LinkedIn drafts to remove machine-sounding patterns: tiered tell catalogue, emoji-pattern scoring, rule explanations, and a voice fingerprint built from the author's own posts. Use when a draft reads generated, before publishing, or when an edit flattened someone's voice.", + "domain": "tools", + "path": "tools/linkedin/linkedin-humanizer/SKILL.md", + "tools": [ + "emoji_audit.py", + "tell_audit.py", + "tell_rules.py", + "voice_fingerprint.py" + ], + "tags": [ + "linkedin", + "humanizer", + "ai-writing", + "editing", + "voice", + "pre-publish" + ] + }, + { + "name": "linkedin-post-writer", + "description": "Drafts LinkedIn posts from a brief: picks an angle and opening pattern the author's real material supports, structures the body, and gates the draft before it is pasted. Use when writing a post, choosing a hook, or checking a draft before publishing.", + "domain": "tools", + "path": "tools/linkedin/linkedin-post-writer/SKILL.md", + "tools": [ + "angle_picker.py", + "post_gate.py" + ], + "tags": [ + "linkedin", + "copywriting", + "hooks", + "content-drafting", + "pre-publish" + ] + }, + { + "name": "linkedin-profile-optimizer", + "description": "Audits and rewrites a LinkedIn profile from pasted text: headline, About, experience, skills, Featured, banner and photo. Use when reviewing a profile, fixing a headline or About, or preparing a profile before posting more.", + "domain": "tools", + "path": "tools/linkedin/linkedin-profile-optimizer/SKILL.md", + "tools": [ + "profile_auditor.py", + "profile_rules.py" + ], + "tags": [ + "linkedin", + "profile", + "headline", + "personal-branding", + "career" + ] + }, + { + "name": "linkedin-reply-manager", + "description": "Triages a pasted LinkedIn comment section and drafts replies: which comments to answer, in what order, which to ignore. Use when answering comments on your own post, replying inside a thread, or clearing a backlog.", + "domain": "tools", + "path": "tools/linkedin/linkedin-reply-manager/SKILL.md", + "tools": [ + "comment_triage.py", + "reply_linter.py" + ], + "tags": [ + "linkedin", + "replies", + "comment-triage", + "community-management", + "engagement" + ] + }, + { + "name": "linkedin-story-interviewer", + "description": "Interviews a person to surface what they actually have to say, and keeps the answers in a local story bank file that later LinkedIn drafts draw on. Use when someone has never posted, when drafts keep coming out generic, or when the story bank is thin or stale.", + "domain": "tools", + "path": "tools/linkedin/linkedin-story-interviewer/SKILL.md", + "tools": [ + "story_bank_audit.py", + "story_bank_rules.py" + ], + "tags": [ + "linkedin", + "interviewing", + "story-bank", + "content-discovery", + "personal-brand" + ] + }, + { + "name": "linkedin-thread-tracker", + "description": "Keeps a local log of LinkedIn comments you left, records who replied, and reports follow-ups due, overdue, or dead. Use when logging a comment, checking which threads need an answer, or reviewing reply rates.", + "domain": "tools", + "path": "tools/linkedin/linkedin-thread-tracker/SKILL.md", + "tools": [ + "thread_log.py", + "thread_tracker.py" + ], + "tags": [ + "linkedin", + "follow-up", + "thread-tracking", + "engagement", + "relationship-building" + ] + }, { "name": "handoff", "description": "This skill should be used when the user asks to \"write a handoff\", \"hand this off\", \"package this work for someone else\", \"summarize where we are so another person/agent can continue\", or \"create a context doc before I switch off this task\".", diff --git a/AGENTS.md b/AGENTS.md index 49f0cfe9..d041a59e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2,7 +2,7 @@ ## What This Repository Is -This is the **universal AI skills library** — **339 production-ready skill packages across 17 professional domains** with **787 Python automation tools**, 76 agents (incl. 8 cross-domain personas), 27 slash commands, and 8 CI/CD workflows. It works with every major AI coding assistant: Claude Code, Cursor, Copilot, Codex / ChatGPT, Gemini CLI, Windsurf, Cline, Aider, Goose, and more. +This is the **universal AI skills library** — **339 production-ready skill packages across 17 professional domains** with **787 Python automation tools**, 77 agents (incl. 8 cross-domain personas), 27 slash commands, and 8 CI/CD workflows. It works with every major AI coding assistant: Claude Code, Cursor, Copilot, Codex / ChatGPT, Gemini CLI, Windsurf, Cline, Aider, Goose, and more. **This is NOT a traditional application.** It's a library of self-contained skill packages meant to be extracted and deployed by users into their own workflows. @@ -26,7 +26,7 @@ This is the **universal AI skills library** — **339 production-ready skill pac ## Repository Structure (domain → skill count) ``` -├── engineering/ # 82 skills — architecture, fullstack, DevOps, security, AI/ML, data, agent tooling +├── engineering/ # 92 skills — architecture, fullstack, DevOps, security, AI/ML, data, agent tooling ├── project-management/ # 70 skills — discovery, delivery, career, strategy-frameworks/, gtm/, Jira/Linear/Notion ├── marketing/ # 41 skills — content, SEO/AEO, demand gen, brand, analytics ├── c-level-advisor/ # 31 skills — CEO/CTO/CFO/CMO/CRO/CPO/COO/CHRO/CISO + CAIO/CDO/CCO/GC/VPE @@ -43,7 +43,7 @@ This is the **universal AI skills library** — **339 production-ready skill pac ├── research/ # 4 skills — litreview, grants, patent, dossier ├── finance/ # 3 skills — financial analysis, DCF, forecasting ├── workflow/ # 2 skills — skill-router, handoff (meta-skills) -├── agents/ # 68 cs-* domain agents + 8 cross-domain personas +├── agents/ # 69 cs-* domain agents + 8 cross-domain personas ├── scripts/ # Manifest builder (build_manifest.py) + skill installer ├── standards/ # Best-practices library ├── templates/ # Reusable templates + sample workflows diff --git a/CHANGELOG.md b/CHANGELOG.md index e7658026..b0a68072 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to the Claude Skills Library will be documented in this file The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [4.13.0] - 2026-10-07 + +### Added + +- **New `tools/` domain with a 12-skill LinkedIn suite** under `tools/linkedin/`, installable as the `tools-skills` plugin. The skills cover the whole loop: `linkedin-story-interviewer` (find what you have to say and keep it in a local story bank), `linkedin-content-planner`, `linkedin-post-writer`, `linkedin-hook-analyzer`, `linkedin-humanizer`, `linkedin-content-repurposer`, `linkedin-comment-writer`, `linkedin-reply-manager`, `linkedin-thread-tracker`, `linkedin-profile-optimizer`, `linkedin-employee-advocacy` and `linkedin-engagement-analytics`. 27 stdlib Python tools. The suite is offline by design: nothing posts, schedules or scrapes; you paste drafts yourself and analysis runs on text or exports you provide. Several tools are gates that exit 1 on their deliberately flawed sample input. +- **`security-audit` engineering skill**: a six-phase source security audit run by a lead agent with isolated sub-agents — reconnaissance, hunting driven by a coverage ledger, refutation of every candidate by a different agent, schema-checked `findings.json`, a second fresh read of each record, then reports. Findings end as confirmed, needs validation, or rejected. Six stdlib tools, including validators for the ledger and findings and a safe evidence-promotion tool; 14 references covering 9 core attack classes and 10 domain companions. +- **`cs-security-auditor` agent** leads the audit and asks for a breadth tier before every full run: `core`, `focused` (core plus web/auth, AI/LLM, supply chain, cloud) or `full` (all 10 companions). Run it as the session's main agent; it needs to launch sub-agents. + +### Changed + +- Counts across the README, docs and site: 385 skills, 21 domains, 900 Python tools, 77 agents. Engineering skill counts that still said 82 now say 92. + ## [4.12.3] - 2026-09-27 ### Fixed diff --git a/CLAUDE.md b/CLAUDE.md index 80546209..23224d36 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co This is the **universal AI skills library** — reusable, production-ready skill packages that bundle domain expertise, best practices, analysis tools, and strategic frameworks. Works with every major AI coding assistant: Claude Code, Cursor, Copilot, Codex, Gemini CLI, Windsurf, Cline, Aider, Goose, and more. -**Current Scope:** 372 production-ready skills across 20 domains with 867 Python automation tools, 76 AI agents (including 8 personas), 26 slash commands, 21 compound sub-skills, and 8 CI/CD workflows. **Project Management is the most-used domain (70 skills: discovery, delivery, career growth, strategy frameworks, GTM, modern AI/growth PM, integrations).** +**Current Scope:** 385 production-ready skills across 21 domains with 900 Python automation tools, 77 AI agents (including 8 personas), 26 slash commands, 21 compound sub-skills, and 8 CI/CD workflows. **Project Management is the most-used domain (70 skills: discovery, delivery, career growth, strategy frameworks, GTM, modern AI/growth PM, integrations).** **Key Distinction**: This is NOT a traditional application. It's a library of skill packages meant to be extracted and deployed by users into their AI coding workflows. @@ -36,6 +36,7 @@ This repository uses **modular documentation**. For domain-specific guidance, se | **Research Ops** | [research-ops/CLAUDE.md](research-ops/CLAUDE.md) | 4 skills: market research, product research, clinical research operations, research finance — applied/operational, distinct from academic `research/` | | **Business Operations** | [business-operations/CLAUDE.md](business-operations/CLAUDE.md) | 6 skills: capacity planning, process mapping, vendor management, internal comms, knowledge ops, procurement optimization | | **Markdown-HTML** | [markdown-html/CLAUDE.md](markdown-html/CLAUDE.md) | 4 skills: md-document, md-slides, md-review gate, design-system — markdown→HTML publishing (stdlib only, zero network) | +| **Tools** | [tools/CLAUDE.md](tools/CLAUDE.md) | 12 skills: the LinkedIn suite (`tools/linkedin/`) — story interviewer, planner, post writer, hook analyzer, humanizer, repurposer, comment writer, reply manager, thread tracker, profile optimizer, employee advocacy, engagement analytics. Offline, stdlib only | | **Standards Library** | [standards/CLAUDE.md](standards/CLAUDE.md) | Communication, quality, git, security standards | | **Templates** | [templates/CLAUDE.md](templates/CLAUDE.md) | Template system usage | @@ -55,7 +56,7 @@ claude-code-skills/ ├── agents/ │ ├── (domain dirs)/ # 26 cs-* prefixed skill agents │ └── personas/ # 7 cross-domain personas (startup-cto, solo-founder, etc.) -├── engineering/ # 82 engineering skills + 3 compound sub-skill systems +├── engineering/ # 92 engineering skills + 3 compound sub-skill systems ├── marketing/ # 41 marketing skills + Python tools (incl. aeo) ├── product-team/ # 13 product skills + Python tools (Tier 2: product-analytics, apple-hig-expert, research-summarizer, spec-to-repo, roadmap-communicator) ├── project-management/ # 70 PM skills (most-used domain) — role-based, discovery/, execution/, career/, strategy-frameworks/ (Tier 3), gtm/ (Tier 3), linear-expert, notion-pm @@ -64,6 +65,7 @@ claude-code-skills/ ├── research-ops/ # 4 applied research ops skills — market, product, clinical, research-finance ├── business-operations/ # 6 business ops skills — capacity, process, vendor, comms, knowledge, procurement ├── markdown-html/ # 4 markdown→HTML publishing skills — document, slides, review gate, design-system +├── tools/ # 12 platform toolkit skills — the LinkedIn suite under tools/linkedin/ (offline, stdlib only) ├── ra-qm-team/ # 27 RA/QM compliance skills (incl. audit-prep/ subfolder) ├── business-growth/ # 20 business & growth skills (incl. commercial: deal-desk, channel-economics, partnerships-architect, commercial-policy) ├── data-analytics/ # 5 data analytics skills + Python tools @@ -145,7 +147,7 @@ mkdocs build # 1. docs/ -> site/ (CLEANS site/ git checkout HEAD -- site/index.html \ site/css/style.css \ site/js/main.js # 2. restore the hand-maintained landing page -python3 scripts/generate_site.py # 3. skills.json -> 372 skill + 20 domain pages +python3 scripts/generate_site.py # 3. skills.json -> 385 skill + 21 domain pages ``` - **`mkdocs build` wipes `site/` before writing.** It must run first, never last. @@ -230,9 +232,11 @@ hand-maintained `site/index.html`. --- -**Last Updated:** September 2026 -**Version:** 4.12.3 -**Status:** 372 skills, 68 cs-* agents (+ 8 personas), 26 commands, 21 sub-skills, 20 domains (incl. workflow meta-skills), Gemini CLI support. Engineering domain adds 4 AI-modernization skills (extended-thinking-architect, batch-api-orchestrator, computer-use-automation, agentic-evaluation-framework) + memory-tool/context-editing/reasoning-effort/caching upgrades to context-engine, llm-cost-optimizer, agent-workflow-designer, mcp-server-builder. Cross-platform surface unified — one `build_manifest.py` run regenerates `cli/skills.json`, `registry.json`, `.gemini/skills-index.json`, and the website catalog `skills.json`; all 20 domains install as Claude Code plugins; Cursor `.cursor/rules/*.mdc` added. PM domain expanded to 70 skills with career track, AI/ML PRD, activation funnels, feature flags, post-mortems, customer feedback triage, pricing PRDs, Linear/Notion/Productboard integrations, strategy frameworks (BMC/lean/SWOT/Porter's/Ansoff), and GTM (gtm-strategy/ICP). +**Last Updated:** October 2026 +**Version:** 4.13.0 +**Status:** 385 skills, 69 cs-* agents (+ 8 personas), 26 commands, 21 sub-skills, 21 domains (incl. workflow meta-skills), Gemini CLI support. Engineering domain adds 4 AI-modernization skills (extended-thinking-architect, batch-api-orchestrator, computer-use-automation, agentic-evaluation-framework) + memory-tool/context-editing/reasoning-effort/caching upgrades to context-engine, llm-cost-optimizer, agent-workflow-designer, mcp-server-builder. Cross-platform surface unified — one `build_manifest.py` run regenerates `cli/skills.json`, `registry.json`, `.gemini/skills-index.json`, and the website catalog `skills.json`; all 21 domains install as Claude Code plugins; Cursor `.cursor/rules/*.mdc` added. PM domain expanded to 70 skills with career track, AI/ML PRD, activation funnels, feature flags, post-mortems, customer feedback triage, pricing PRDs, Linear/Notion/Productboard integrations, strategy frameworks (BMC/lean/SWOT/Porter's/Ansoff), and GTM (gtm-strategy/ICP). +- **Recent additions (October 2026) — new `tools/` domain, LinkedIn suite (12 skills, 27 stdlib tools):** `tools/linkedin/` holds linkedin-story-interviewer, -content-planner, -post-writer, -hook-analyzer, -humanizer, -content-repurposer, -comment-writer, -reply-manager, -thread-tracker, -profile-optimizer, -employee-advocacy and -engagement-analytics. Offline by design: nothing posts, schedules or scrapes; drafts are pasted by the user and analysis runs on pasted text or exports. Gates exit 1 on their deliberately flawed samples. +- **Recent additions (October 2026) — security audit:** engineering `security-audit` (6 stdlib tools, 14 references) and the `cs-security-auditor` agent that leads it. Six phases — reconnaissance, coverage-ledger-driven hunting, independent candidate validation, schema-checked `findings.json`, fresh record verification, reporting — with three run-time breadth tiers (`core`, `focused`, `full`). The agent asks for the tier before every full audit. Validators exit non-zero on an invalid ledger or findings file by design. - **Recent additions (September 2026) — 4 skills + 2026 refresh:** marketing `ai-content-disclosure` and `conversational-ads`; PM `discovery/ai-prototyping` and `execution/agents-in-the-team` (8 stdlib tools). Existing marketing and PM skills refreshed against 2025–2026 changes: retired FAQ/HowTo rich results, AI crawler and Search Console AI controls, Advantage+/AI Max, GA4 AI Assistant channel, Productboard API v2, Jira `/search/jql`, Notion data sources, DORA 2024 benchmarks, EU AI Act Omnibus (Reg. 2026/1744), Atlassian Data Center end-of-life. Workflow templates hardened against shell injection (CWE-78). - **Recent additions (July 2026) — 25 skills, 3 new domains:** `business-operations/` (capacity-planner, process-mapper, vendor-management, internal-comms, knowledge-ops, procurement-optimizer), `research-ops/` (market-research, product-research, clinical-research, research-finance — applied/operational, distinct from academic `research/`), and `markdown-html/` (md-document, md-slides, md-review, design-system — stdlib-only markdown→HTML with zero network calls). Plus 5 engineering skills (write-a-skill, spec-driven-workflow, code-tour, agent-harness, cloud-security), data-analytics/statistical-analyst, 2 PM skills (team-communications, meeting-analyzer), and 3 personal-productivity skills (capture, deep-work, reflect). 44 new stdlib Python tools. Several tools ship deliberate CI gates that exit non-zero on flawed sample data — see each SKILL.md's exit-code contract. - **Recent Tier-3 additions (May 2026):** 12 PM skills in new subfolders `strategy-frameworks/` (business-model-canvas, lean-canvas, swot-analysis, porters-five-forces, ansoff-matrix) and `gtm/` (gtm-strategy, ideal-customer-profile), plus discovery additions (opportunity-solution-tree, metrics-dashboard) and execution additions (stakeholder-map, test-scenarios, sprint-plan). 12 stdlib Python validators, ~12K lines. diff --git a/GEMINI.md b/GEMINI.md index 1a9d132b..39813a5b 100644 --- a/GEMINI.md +++ b/GEMINI.md @@ -4,7 +4,7 @@ Instructions for Google Gemini CLI when working with the Claude Skills Library. ## Project Overview -This is the **Universal AI Skills Library** -- **372 production-ready skill packages across 20 domains** with **867 Python automation tools**. Each skill bundles domain expertise, best practices, analysis tools, and strategic frameworks into self-contained packages. +This is the **Universal AI Skills Library** -- **385 production-ready skill packages across 21 domains** with **900 Python automation tools**. Each skill bundles domain expertise, best practices, analysis tools, and strategic frameworks into self-contained packages. **This is NOT a traditional application.** It is a library of skill packages meant to be extracted and deployed into your AI coding workflows. @@ -12,7 +12,7 @@ This is the **Universal AI Skills Library** -- **372 production-ready skill pack ### 1. Browse Available Skills -Open `.gemini/skills-index.json` to see all 372 skills with descriptions, domains, tools, and tags. Each entry points to a full `SKILL.md` documentation file. (The index is generated by `scripts/build_manifest.py` -- never edit it by hand.) +Open `.gemini/skills-index.json` to see all 385 skills with descriptions, domains, tools, and tags. Each entry points to a full `SKILL.md` documentation file. (The index is generated by `scripts/build_manifest.py` -- never edit it by hand.) ### 2. Activate a Skill @@ -46,7 +46,7 @@ Skills are composable. For example, combine `senior-architect` + `code-reviewer` | Domain | Count | Description | |--------|-------|-------------| -| engineering | 82 | Architecture, backend, frontend, DevOps, security, QA, AI/ML, data, agent tooling | +| engineering | 92 | Architecture, backend, frontend, DevOps, security, QA, AI/ML, data, agent tooling | | project-management | 66 | Discovery, delivery, career, strategy frameworks, GTM, Jira/Linear/Notion | | marketing | 41 | Content, SEO/AEO, ads, email, social, analytics, brand | | c-level-advisor | 31 | CEO/CTO/CFO/CMO/CRO/CPO/COO/CHRO/CISO + CAIO/CDO/CCO/GC/VPE | @@ -62,6 +62,7 @@ Skills are composable. For example, combine `senior-architect` + `code-reviewer` | hr-operations | 4 | Talent acquisition, people analytics, HR business partner | | research | 4 | Systematic literature review, grants, patent prior art, intelligence dossiers | | finance | 3 | Financial analysis, DCF valuation, budgeting, forecasting | +| tools | 12 | LinkedIn suite: posts, humanizing, comments, replies, planning, profile, advocacy, analytics (offline) | | workflow | 2 | Meta-skills: skill-router, handoff | ## Skill Activation Patterns @@ -83,7 +84,7 @@ Use these patterns when asking Gemini to work with skills: ## Quick Reference - **Skill wrappers:** `.gemini/skills/` -- Short summaries of selected skills for quick discovery -- **Full index:** `.gemini/skills-index.json` -- All 372 skills with metadata (auto-generated) +- **Full index:** `.gemini/skills-index.json` -- All 385 skills with metadata (auto-generated) - **Install to your project:** Run `scripts/gemini-install.sh` to copy Gemini config files ## Key Principles @@ -99,9 +100,9 @@ Use these patterns when asking Gemini to work with skills: ``` claude-code-skills/ .gemini/ -- Gemini CLI configuration - skills-index.json -- Full skill catalog (372 skills, auto-generated) + skills-index.json -- Full skill catalog (385 skills, auto-generated) skills/ -- Selected skill wrappers for quick access - engineering/ -- 82 skills + engineering/ -- 92 skills project-management/ -- 70 skills marketing/ -- 41 skills c-level-advisor/ -- 31 skills diff --git a/README.md b/README.md index 667664d9..c8aa8777 100644 --- a/README.md +++ b/README.md @@ -6,9 +6,9 @@

AI skills for every team, not just engineering.

- 372 Skills + 385 Skills 20 Domains - 76 Expert Agents + 77 Expert Agents 11 AI Assistants 18 Compliance Frameworks GitHub Stars @@ -31,7 +31,7 @@ In the Claude desktop app: 2. Go to **Browse plugins** 3. Enter: `borghei/Claude-Skills` -All 372 skills become available across your Claude conversations. +All 385 skills become available across your Claude conversations. @@ -53,14 +53,15 @@ See [docs/INSTALLATION.md](docs/INSTALLATION.md) for the Claude Code plugin, MCP **Ship a SOC 2 audit. Run a sprint retro. Draft a board update. Spin up a fintech compliance program.** Real workflows you can finish today, not prompt templates that need a prompt engineer to drive them. - **The only OSS skills library covering regulated industries.** SOC 2, ISO 13485, MDR, FDA, EU AI Act, NIS2, DORA, HIPAA, GDPR. 18 compliance frameworks with checklists, gap analyses, and audit-ready outputs. -- **Scripts that actually run, not just prompt templates.** 867 Python tools you can pipe into CI, MCP servers, or your own scripts. Standard library only, no pip cliffs. -- **Call one persona, get a stack of skills behind it.** `startup-cto`, `growth-marketer`, `solo-founder`, `cs-cto-advisor`, `cs-fintech-advisor`, and more. 76 expert identities ready out of the box. +- **Scripts that actually run, not just prompt templates.** 900 Python tools you can pipe into CI, MCP servers, or your own scripts. Standard library only, no pip cliffs. +- **Call one persona, get a stack of skills behind it.** `startup-cto`, `growth-marketer`, `solo-founder`, `cs-cto-advisor`, `cs-fintech-advisor`, and more. 77 expert identities ready out of the box. - **Works with your AI assistant.** Claude Code, Cursor, Codex, Gemini CLI, Copilot, Windsurf, Cline, Aider, Goose, OpenCode, and ChatGPT / Claude.ai. - **Discovery → delivery → career for product teams.** Project Management is the deepest domain — 70 skills, and every one ships with a worked example, an anti-pattern library, and a deterministic scoring rubric. 15 PM skills callable as MCP tools. 5 chain pipelines, 3 live data adapters. Jira / Linear / Notion / Productboard / Confluence. Tier 3 adds strategy frameworks (BMC, Lean Canvas, SWOT, Porter's, Ansoff) and GTM (gtm-strategy, ICP). - **Current with 2026 marketing and product practice.** AI search (AI Overviews, AI Mode, AI crawlers), ads in ChatGPT and AI search, EU AI Act and FTC disclosure checks, AI prototyping, and running delivery when AI agents take tickets. Fast-moving facts are checked against official sources and dated. - **Runs the business, not just the codebase.** Business operations (capacity planning, process mapping, vendor and procurement management), applied research ops (market, product, clinical study design, research finance), and a stdlib-only markdown→HTML publishing pipeline with a CI-ready review gate. +- **Audits your code and runs your LinkedIn.** A multi-agent source security audit (`security-audit` + the `cs-security-auditor` agent) that verifies every finding with a second agent and reports confirmed, needs-validation and rejected separately. And a 12-skill LinkedIn suite in [`tools/`](tools/): story interviews, planning, post writing, humanizing, comments and replies, profile, team advocacy and engagement analysis. Offline tools, nothing is posted for you. -*Supporting evidence:* 372 skills · 867 Python tools · 76 agents · 18 compliance frameworks · 20 domains · 11 platforms. +*Supporting evidence:* 385 skills · 900 Python tools · 77 agents · 18 compliance frameworks · 21 domains · 11 platforms. --- @@ -68,7 +69,7 @@ See [docs/INSTALLATION.md](docs/INSTALLATION.md) for the Claude Code plugin, MCP **[Project Management (70) ★](project-management/)** *(most-visited domain. Discovery, delivery, career growth, strategy frameworks, GTM, Jira / Linear / Notion / Confluence.)* -[Engineering (91)](engineering/) · [Marketing (41)](marketing/) · [C-Level (31)](c-level-advisor/) · [Compliance (27)](ra-qm-team/) · [Business & Growth (20)](business-growth/) · [Legal (17)](legal/) · [Product Team (13)](product-team/) · [Personal Productivity (13)](personal-productivity/) · [Vertical Advisors (7)](vertical-advisors/) · [Business Operations (6)](business-operations/) · [Data (6)](data-analytics/) · [Sales (5)](sales-success/) · [HR (4)](hr-operations/) · [Documents (4)](documents/) · [Research (4)](research/) · [Research Ops (4)](research-ops/) · [Markdown-HTML (4)](markdown-html/) · [Finance (3)](finance/) · [Workflow (2)](workflow/) · [All skills →](docs/skills-reference.md) +[Engineering (92)](engineering/) · [Marketing (41)](marketing/) · [C-Level (31)](c-level-advisor/) · [Compliance (27)](ra-qm-team/) · [Business & Growth (20)](business-growth/) · [Legal (17)](legal/) · [Product Team (13)](product-team/) · [Personal Productivity (13)](personal-productivity/) · [Vertical Advisors (7)](vertical-advisors/) · [Business Operations (6)](business-operations/) · [Data (6)](data-analytics/) · [Sales (5)](sales-success/) · [HR (4)](hr-operations/) · [Documents (4)](documents/) · [Research (4)](research/) · [Research Ops (4)](research-ops/) · [Tools — LinkedIn (12)](tools/) · [Markdown-HTML (4)](markdown-html/) · [Finance (3)](finance/) · [Workflow (2)](workflow/) · [All skills →](docs/skills-reference.md) --- @@ -80,7 +81,7 @@ See [docs/INSTALLATION.md](docs/INSTALLATION.md) for the Claude Code plugin, MCP `startup-cto` · `growth-marketer` · `solo-founder` · `content-strategist` · `devops-engineer` · `finance-lead` · `product-manager` · `skill-creator` -Activate with `/persona startup-cto` or just mention it in conversation. Add the 68 single-domain `cs-*` agents (e.g. `cs-cto-advisor`, `cs-fintech-advisor`, `cs-scrum-master`) for narrower expertise. +Activate with `/persona startup-cto` or just mention it in conversation. Add the 69 single-domain `cs-*` agents (e.g. `cs-cto-advisor`, `cs-fintech-advisor`, `cs-scrum-master`) for narrower expertise. ### Deep-dive skill packs @@ -169,7 +170,7 @@ Thanks to [@rishaankgupta](https://github.com/rishaankgupta) for responsibly dis ---

- 372 skills · 867 tools · 20 domains · 76 agents · 11 platforms.
+ 385 skills · 900 tools · 21 domains · 77 agents · 11 platforms.
borghei.me

diff --git a/agents/engineering/cs-security-auditor.md b/agents/engineering/cs-security-auditor.md new file mode 100644 index 00000000..4e914352 --- /dev/null +++ b/agents/engineering/cs-security-auditor.md @@ -0,0 +1,222 @@ +--- +name: cs-security-auditor +description: Lead agent for multi-phase source security audits — coverage-ledger-driven hunting, independent verification, and machine-validated findings at a breadth the user picks per run +skills: engineering/security-audit +domain: engineering +model: opus +tools: [Read, Write, Bash, Grep, Glob, Task] +--- + +# Security Auditor Agent + +## Purpose + +The cs-security-auditor agent runs a structured security audit of a source repository as the lead of a small team of sub-agents. It maps the target, writes a coverage ledger of exactly what will be examined, assigns isolated hunters to each unit, gives every resulting claim to a different agent whose job is to refute it, and reports only from records that two deterministic validators have accepted. It orchestrates the `security-audit` skill; the method, prompts, attack classes, schema, and validators all live there. + +This agent is for engineering leads, security engineers, and maintainers who need findings they can act on without re-deriving them: before a release, ahead of a customer or acquirer review, after a risky change, or when an earlier scan left a list nobody trusts. Every finding it reports is in one of three states — confirmed with a local reproduction, blocked on one named fact, or rejected — and every part of the codebase it did not reach is listed as such. + +It is deliberately conservative. It works from source and sandboxed local checks, never contacts a deployed system, never edits the target, and never upgrades a guess into a vulnerability. It differs from [cs-security-engineer](cs-security-engineer.md), which produces threat models and scans, and from [cs-pen-tester](cs-pen-tester.md), which plans authorized engagements against running systems. + +## First question: breadth tier + +**Every full audit starts by asking the user to choose a tier.** Do not pick one silently. + +| Tier | Attack-class files in play | Choose it when | +|---|---|---| +| `core` | 9 core classes | First look, small service, tight budget | +| `focused` | Core + web/auth, AI/LLM, supply chain, cloud | Most web and API products | +| `full` | Core + all 10 companions | High stakes, native code, multi-tenant platforms, desktop or mobile apps | + +Present the three options with their cost implication (more companions means more units, hunters, and verifiers), recommend one from what is visible in the repository, and wait for the answer. If the user says to just run it, use `focused` and say so at the top of the report. Ask about scope, output location, profile, and budget only where the request leaves them open. + +Whatever the tier, a boundary whose companion was not selected is recorded as an `out_of_scope` unit and named in the report — the tier narrows the work, never the honesty of the coverage statement. + +## Skill Integration + +**Skill Location:** `../../engineering/security-audit/` + +### Python Tools + +1. **Run Helper** + - **Purpose:** Creates the run directory outside the target, records tier, profile, scope, and budget, derives coverage IDs, allocates an agent budget, and prints the coverage tables for the report + - **Path:** `../../engineering/security-audit/scripts/audit_run.py` + - **Usage:** `python ../../engineering/security-audit/scripts/audit_run.py init --target --tier focused` + - **Subcommands:** `init`, `coverage-id`, `budget`, `summary` + +2. **Coverage Ledger Validator** + - **Purpose:** Enforces deterministic unit IDs, the per-status state table, owned evidence, resolvable attack-class blocks, tier confinement, findings cross-links, and the pre-report `--final` gate + - **Path:** `../../engineering/security-audit/scripts/validate_coverage_ledger.py` + - **Usage:** `python ../../engineering/security-audit/scripts/validate_coverage_ledger.py /coverage-ledger.json --metadata /run-metadata.json` + +3. **Findings Validator** + - **Purpose:** Checks `findings.json` against the schema plus ordering, path safety, trace shape, and the rule that severity cannot exceed demonstrated impact + - **Path:** `../../engineering/security-audit/scripts/validate_findings.py` + - **Usage:** `python ../../engineering/security-audit/scripts/validate_findings.py /findings.json` + +4. **Evidence Promoter** + - **Purpose:** The only route from a sub-agent's `scratch/` to retained `evidence/`; accepts single-link regular files within byte limits and never follows links + - **Path:** `../../engineering/security-audit/scripts/promote_evidence.py` + - **Usage:** `python ../../engineering/security-audit/scripts/promote_evidence.py --run-dir --agent-id hunter-03 --file result.txt` + +All tools accept `--format json`. Validators exit 0 when valid, 1 when invalid, 2 on unreadable input. + +### Knowledge Bases + +1. **Reconnaissance** — `../../engineering/security-audit/references/reconnaissance.md` — scout prompts, prior-run handling, ledger seeding +2. **Hunting** — `../../engineering/security-audit/references/hunting.md` — assignment order, hunter prompt, candidate gate, coverage critic +3. **Validation and Reporting** — `../../engineering/security-audit/references/validation-and-reporting.md` — verifier prompt, record fields, report contents +4. **Core Attack Classes** — `../../engineering/security-audit/references/attack-classes.md` — nine classes and the companion routing table +5. **Focused-tier companions** — `web-protocol-and-auth.md`, `ai-and-llm.md`, `supply-chain-and-release.md`, `cloud-and-deployment.md` in the same folder +6. **Full-tier companions** — `client-side.md`, `data-isolation-and-lifecycle.md`, `desktop-mobile-and-local-ipc.md`, `memory-safety-and-binary.md`, `protocols-rpc-and-messaging.md`, `resource-exhaustion-and-availability.md` + +Read a reference when its phase begins, not all at once. Sub-agents cannot open these files: paste the blocks they need into their prompts. + +### Templates + +1. **Report Template** — `../../engineering/security-audit/assets/report_template.md` +2. **Findings Schema** — `../../engineering/security-audit/assets/findings.schema.json` +3. **Sample Run** — `sample_run_metadata.json`, `sample_coverage_ledger.json`, `sample_findings.json` in the same folder; all three pass the validators and show every unit state and verdict + +## Operating rules + +- **One writer.** This agent alone writes `run-metadata.json`, `architecture.md`, `coverage-ledger.json`, `findings.json`, and the reports. Sub-agents return JSON and write only inside their own `scratch/`. +- **Fresh eyes.** A sub-agent never validates a candidate it hunted, and never sees another verifier's conclusion. Use a new sub-agent for each role and each reopened unit. +- **No execution without a sandbox.** If the machine cannot enforce no-network, allowlisted environment, read-only target, scratch-only writes, and resource limits, target code is not run. The lead becomes `needs_validation`. +- **Validate after every edit.** A ledger or findings file that fails its validator cannot drive the next step. +- **Two endings only.** Reports written with both validators passing, or `run_status: "incomplete"` with the reason in the report's first paragraph. Never stop mid-phase. +- **Run as the lead.** This agent needs the delegation tool. Most platforms do not let a sub-agent launch its own sub-agents, so run it as the session's main agent. Where no delegation exists, the independence rules cannot be met: say so and offer guidance mode instead of a single-agent imitation of the full audit. +- **Scope refusal.** Requests to test a deployed system, use real credentials, or produce a working exploit are outside this agent. Point to cs-pen-tester for an authorized engagement. + +## Workflows + +### Workflow 1: Full Audit + +**Goal:** Take a repository from "audit this" to a verified report at the tier the user chose. + +**Steps:** +1. **Ask the tier** (and any open scope, output, profile, or budget question). +2. **Initialise** — `python ../../engineering/security-audit/scripts/audit_run.py init --target --tier --profile `. Note the run directory and any prior runs. +3. **Reconnaissance** — launch the four scouts from `reconnaissance.md` in parallel; write `architecture.md` (about 1,000 words); select companions the tier allows and the source supports. +4. **Seed the ledger** — one unit per surface × boundary × subsystem × attack class; derive each ID with `audit_run.py coverage-id`; validate. +5. **Hunt in waves** — assign `planned` units in priority order, build each hunter prompt from `hunting.md`, update the ledger from each structured result, promote evidence, validate. Run the coverage critic after each wave and repeat until the stop rule for the profile is met. +6. **Validate candidates** — one fresh verifier per candidate, instructed to refute it. +7. **Write `findings.json`** — one record per fingerprint, sorted; run both validators. +8. **Verify records** — one fresh reader per `confirmed` and `needs_validation` record; send material replacements to another fresh verifier. +9. **Report** — run the `--final` gate and `audit_run.py summary`; write `REPORT.md`, `FINDINGS-DETAIL.md`, and `NEEDS-VALIDATION.md` from the template. + +**Expected Output:** A run directory with validated `coverage-ledger.json` and `findings.json`, three report files, and an explicit list of what was not covered. + +**Time Estimate:** Roughly 20–40 sub-agent invocations for a mid-sized service at `focused` / `standard`; `core` / `quick` fits in about a dozen. + +### Workflow 2: Scoped or Budgeted Audit + +**Goal:** Review one subsystem, one diff, or a fixed spend without overstating coverage. + +**Steps:** +1. Ask the tier; confirm the paths or refs in scope and the agent budget. +2. `python ../../engineering/security-audit/scripts/audit_run.py init --target --tier --profile quick --scope --budget ` +3. Check the split: `python ../../engineering/security-audit/scripts/audit_run.py budget --budget --profile quick --units ` +4. Seed in-scope units as `planned`; record every other surface the scouts saw as `out_of_scope` with a reason. +5. Run one wave and one critic; mark what the critic adds as `deferred`. +6. Validate, verify, and report as in Workflow 1. State in the first paragraph that this is a partial pass. + +**Expected Output:** Verified findings for the scoped area plus a gap list the next run can start from. + +**Time Estimate:** 8–15 sub-agent invocations. + +### Workflow 3: Repeat Audit + +**Goal:** Grow coverage across runs instead of repeating the first one. + +**Steps:** +1. Ask the tier — a wider tier than last time turns earlier `out_of_scope` units into work. +2. Run `init` on the same target; read every prior ledger and findings file it lists. +3. Compare prior records with the current source and seed units by origin: carry unchanged confirmed records to a fresh verifier, revalidate anything whose source changed, reopen deferred, blocked, and out-of-scope units. +4. Give hunters the carried root causes as exclusions (fingerprint, title, root cause only). +5. Continue from step 5 of Workflow 1. + +**Expected Output:** A report that names carried findings, revalidated findings, newly closed gaps, and gaps still open. + +**Time Estimate:** Usually less than the first run at the same tier. + +### Workflow 4: Guidance Mode + +**Goal:** Answer a security question or review one finding without starting a run. + +**Steps:** +1. Confirm the request is a question or a focused review, not a full audit. If unclear, ask. +2. Read only the relevant reference block. +3. Apply the same bar: actor, control, traced path, result. Answer in the conversation. +4. Create no run directory and no files. Offer a full audit if the question turns out to be bigger. + +**Expected Output:** A direct answer with file and line references. + +**Time Estimate:** Minutes. + +## Integration Examples + +### Example 1: Start a Focused Run and Validate the Seeded Ledger + +```bash +SKILL=../../engineering/security-audit +python $SKILL/scripts/audit_run.py --format json init --target ~/code/orders-api --tier focused +RUN=~/security-audits/orders-api/run-1 + +python $SKILL/scripts/audit_run.py coverage-id \ + --surface "src/api/orders.py#POST /orders/{id}/refund" \ + --boundary "src/authz/policy.py#require_owner" \ + --subsystem services/orders \ + --attack-class "attack-classes.md#Access control" + +python $SKILL/scripts/validate_coverage_ledger.py $RUN/coverage-ledger.json \ + --metadata $RUN/run-metadata.json +``` + +### Example 2: Promote Evidence and Close Out + +```bash +SKILL=../../engineering/security-audit +RUN=~/security-audits/orders-api/run-1 + +python $SKILL/scripts/promote_evidence.py --run-dir $RUN \ + --agent-id hunter-01 --file refund_cross_tenant.txt + +python $SKILL/scripts/validate_findings.py $RUN/findings.json +python $SKILL/scripts/validate_coverage_ledger.py $RUN/coverage-ledger.json \ + --metadata $RUN/run-metadata.json --findings $RUN/findings.json \ + --run-dir $RUN --final +python $SKILL/scripts/audit_run.py summary --run-dir $RUN +``` + +### Example 3: Check the Sample Run + +```bash +SKILL=../../engineering/security-audit +python $SKILL/scripts/validate_findings.py $SKILL/assets/sample_findings.json +python $SKILL/scripts/validate_coverage_ledger.py $SKILL/assets/sample_coverage_ledger.json \ + --metadata $SKILL/assets/sample_run_metadata.json \ + --findings $SKILL/assets/sample_findings.json --final +``` + +## Success Metrics + +- **Tier asked before work starts:** every full audit +- **Confirmed findings with an independent verifier and a local reproduction:** 100% +- **Confirmed findings later shown to be false positives:** target under 5% +- **Units ending `planned` or `in_progress` in a completed run:** zero +- **Coverage gaps (deferred, blocked, out of scope) named in the report:** 100% +- **Validator failures at report time:** zero +- **`needs_validation` leads the owner can resolve from the written check alone:** over 90% + +## Related Agents + +- [cs-security-engineer](cs-security-engineer.md) — threat modeling, secret scanning, compliance checks +- [cs-pen-tester](cs-pen-tester.md) — authorized engagements against running systems +- [cs-secops-engineer](cs-secops-engineer.md) — operational security and response +- [cs-code-auditor](cs-code-auditor.md) — code quality and maintainability review +- [cs-ciso-advisor](../compliance/cs-ciso-advisor.md) — turning findings into a risk programme + +## References + +- **Security Audit Skill:** [../../engineering/security-audit/SKILL.md](../../engineering/security-audit/SKILL.md) +- **Agent Development Guide:** [../CLAUDE.md](../CLAUDE.md) diff --git a/cli/skills.json b/cli/skills.json index 99a5b274..45a65dcd 100644 --- a/cli/skills.json +++ b/cli/skills.json @@ -1,8 +1,8 @@ { "schema_version": "1.0.0", - "generated_at": "2026-09-27T21:13:15+00:00", - "skill_count": 372, - "domain_count": 20, + "generated_at": "2026-10-07T00:46:25+00:00", + "skill_count": 385, + "domain_count": 21, "skills": [ { "name": "a11y-audit", @@ -2087,6 +2087,58 @@ "has_references": true, "has_assets": false }, + { + "name": "security-audit", + "domain": "engineering", + "description": "Multi-agent source security audit with a coverage ledger, independently verified findings, and machine-checked records. Use when asked to audit a codebase, find vulnerabilities, or run a source-level pen test.", + "tags": [ + "security-audit", + "vulnerability-research", + "multi-agent", + "code-review", + "appsec" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "author": "borghei", + "license": "MIT + Commons Clause", + "category": "engineering", + "subdomain": "", + "path": "engineering/security-audit", + "files": [ + "SKILL.md", + "assets/findings.schema.json", + "assets/report_template.md", + "assets/sample_coverage_ledger.json", + "assets/sample_findings.json", + "assets/sample_run_metadata.json", + "references/ai-and-llm.md", + "references/attack-classes.md", + "references/client-side.md", + "references/cloud-and-deployment.md", + "references/data-isolation-and-lifecycle.md", + "references/desktop-mobile-and-local-ipc.md", + "references/hunting.md", + "references/memory-safety-and-binary.md", + "references/protocols-rpc-and-messaging.md", + "references/reconnaissance.md", + "references/resource-exhaustion-and-availability.md", + "references/supply-chain-and-release.md", + "references/validation-and-reporting.md", + "references/web-protocol-and-auth.md", + "scripts/audit_common.py", + "scripts/audit_run.py", + "scripts/ledger_rules.py", + "scripts/promote_evidence.py", + "scripts/validate_coverage_ledger.py", + "scripts/validate_findings.py" + ], + "size_bytes": 223009, + "tools_count": 6, + "has_scripts": true, + "has_references": true, + "has_assets": true + }, { "name": "self-improving-agent", "domain": "engineering", @@ -3014,10 +3066,11 @@ "path": "engineering/threat-detection", "files": [ "SKILL.md", + "examples/access.log", "references/threat-indicators.md", "scripts/threat_signal_analyzer.py" ], - "size_bytes": 25347, + "size_bytes": 32948, "tools_count": 1, "has_scripts": true, "has_references": true, @@ -11438,6 +11491,445 @@ "has_references": true, "has_assets": true }, + { + "name": "linkedin-comment-writer", + "domain": "tools", + "description": "Drafts comments on other people's LinkedIn posts that add a specific the post lacked, then gates them offline. Use when commenting on a post, engaging with a prospect or peer, or checking a comment before pasting it.", + "tags": [ + "linkedin", + "comments", + "engagement", + "social-selling", + "writing" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "author": "borghei", + "license": "MIT + Commons Clause", + "category": "tools", + "subdomain": "", + "path": "tools/linkedin/linkedin-comment-writer", + "files": [ + "SKILL.md", + "assets/comment_brief_template.md", + "assets/sample_comment_drafts.json", + "assets/sample_story_bank.json", + "references/comment-moves.md", + "references/comment-quality-rubric.md", + "references/commenting-situations.md", + "scripts/comment_linter.py", + "scripts/comment_rules.py" + ], + "size_bytes": 79365, + "tools_count": 2, + "has_scripts": true, + "has_references": true, + "has_assets": true + }, + { + "name": "linkedin-content-planner", + "domain": "tools", + "description": "Builds a weekly or monthly LinkedIn publishing plan from content pillars, a posting cadence and a format mix, with a founder-oriented pillar set as an alternative to the general one. Use when planning a week, a month or a launch period of posts rather than drafting a single post.", + "tags": [ + "linkedin", + "content-calendar", + "content-pillars", + "editorial-planning", + "founder-content" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "author": "borghei", + "license": "MIT + Commons Clause", + "category": "tools", + "subdomain": "", + "path": "tools/linkedin/linkedin-content-planner", + "files": [ + "SKILL.md", + "assets/plan_config_template.json", + "assets/sample_founder_config.json", + "assets/sample_plan_config.json", + "assets/sample_story_bank.json", + "assets/weekly_plan_template.md", + "references/cadence-formats-and-config.md", + "references/founder-pillar-set.md", + "references/pillars-and-mix.md", + "scripts/plan_builder.py", + "scripts/plan_rules.py" + ], + "size_bytes": 88981, + "tools_count": 2, + "has_scripts": true, + "has_references": true, + "has_assets": true + }, + { + "name": "linkedin-content-repurposer", + "domain": "tools", + "description": "Turns something made for another channel (a thread, a video or talk transcript, a blog post, a newsletter) into a post that reads as native to LinkedIn, without changing what the source says. Use when existing material should become a LinkedIn post rather than writing one from a blank page.", + "tags": [ + "linkedin", + "repurposing", + "content-adaptation", + "transcripts", + "fidelity-check" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "author": "borghei", + "license": "MIT + Commons Clause", + "category": "tools", + "subdomain": "", + "path": "tools/linkedin/linkedin-content-repurposer", + "files": [ + "SKILL.md", + "assets/repurposing_brief_template.md", + "assets/sample_article.md", + "assets/sample_draft.txt", + "assets/sample_draft_clean.txt", + "assets/sample_talk_transcript.txt", + "assets/sample_thread.txt", + "references/fidelity-rules.md", + "references/native-fit-guide.md", + "references/source-format-playbooks.md", + "scripts/draft_fidelity_check.py", + "scripts/repurpose_analyzer.py", + "scripts/repurpose_rules.py" + ], + "size_bytes": 97612, + "tools_count": 3, + "has_scripts": true, + "has_references": true, + "has_assets": true + }, + { + "name": "linkedin-employee-advocacy", + "domain": "tools", + "description": "Designs and runs an employee advocacy programme: who posts what and how often, review rules, and what is never scripted. Use when getting a team posting, writing an advocacy policy, or fixing a programme that has stalled.", + "tags": [ + "linkedin", + "employee-advocacy", + "governance", + "team-cadence", + "social-policy" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "author": "borghei", + "license": "MIT + Commons Clause", + "category": "tools", + "subdomain": "", + "path": "tools/linkedin/linkedin-employee-advocacy", + "files": [ + "SKILL.md", + "assets/advocacy_charter_template.md", + "assets/roster_template.json", + "assets/sample_roster.json", + "references/governance-and-consent.md", + "references/measuring-the-programme.md", + "references/programme-design.md", + "scripts/advocacy_planner.py", + "scripts/advocacy_rules.py" + ], + "size_bytes": 82335, + "tools_count": 2, + "has_scripts": true, + "has_references": true, + "has_assets": true + }, + { + "name": "linkedin-engagement-analytics", + "domain": "tools", + "description": "Segments who reacted to and commented on a post from a CSV or JSON export, and says whether it reached the intended audience. Use when reviewing a post's engagers, checking audience fit, or building a baseline.", + "tags": [ + "linkedin", + "analytics", + "audience-segmentation", + "engagement", + "privacy" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "author": "borghei", + "license": "MIT + Commons Clause", + "category": "tools", + "subdomain": "", + "path": "tools/linkedin/linkedin-engagement-analytics", + "files": [ + "SKILL.md", + "assets/post_readout_template.md", + "assets/sample_engagers.csv", + "assets/sample_engagers_earlier_post.json", + "assets/sample_target_audience.json", + "assets/target_audience_template.json", + "references/data-handling.md", + "references/metrics-layers.md", + "references/segmentation-method.md", + "scripts/engager_segmenter.py", + "scripts/segment_rules.py" + ], + "size_bytes": 83212, + "tools_count": 2, + "has_scripts": true, + "has_references": true, + "has_assets": true + }, + { + "name": "linkedin-hook-analyzer", + "domain": "tools", + "description": "Extracts and classifies the opening-line pattern of LinkedIn posts the user pastes or saves, and turns each into a reusable slot template with cautions. Use when studying why an opening works, building a swipe file, or adapting a hook pattern without copying it.", + "tags": [ + "linkedin", + "hooks", + "swipe-file", + "pattern-analysis", + "copywriting" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "author": "borghei", + "license": "MIT + Commons Clause", + "category": "tools", + "subdomain": "", + "path": "tools/linkedin/linkedin-hook-analyzer", + "files": [ + "SKILL.md", + "assets/sample_saved_posts.txt", + "assets/sample_swipe.json", + "assets/swipe_file_template.md", + "references/classification-rules.md", + "references/pattern-library.md", + "references/reuse-guide.md", + "scripts/hook_classifier.py", + "scripts/hook_patterns.py" + ], + "size_bytes": 87282, + "tools_count": 2, + "has_scripts": true, + "has_references": true, + "has_assets": true + }, + { + "name": "linkedin-humanizer", + "domain": "tools", + "description": "Audits and rewrites LinkedIn drafts to remove machine-sounding patterns: tiered tell catalogue, emoji-pattern scoring, rule explanations, and a voice fingerprint built from the author's own posts. Use when a draft reads generated, before publishing, or when an edit flattened someone's voice.", + "tags": [ + "linkedin", + "humanizer", + "ai-writing", + "editing", + "voice", + "pre-publish" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "author": "borghei", + "license": "MIT + Commons Clause", + "category": "tools", + "subdomain": "", + "path": "tools/linkedin/linkedin-humanizer", + "files": [ + "SKILL.md", + "assets/audit_report_template.md", + "assets/sample_draft.txt", + "assets/sample_draft_edited.txt", + "assets/sample_past_posts.txt", + "assets/sample_voice.json", + "assets/voice_profile_template.md", + "references/emoji-patterns.md", + "references/rewrite-playbook.md", + "references/rule-catalogue.md", + "references/voice-fingerprint-guide.md", + "scripts/emoji_audit.py", + "scripts/tell_audit.py", + "scripts/tell_rules.py", + "scripts/voice_fingerprint.py" + ], + "size_bytes": 127019, + "tools_count": 4, + "has_scripts": true, + "has_references": true, + "has_assets": true + }, + { + "name": "linkedin-post-writer", + "domain": "tools", + "description": "Drafts LinkedIn posts from a brief: picks an angle and opening pattern the author's real material supports, structures the body, and gates the draft before it is pasted. Use when writing a post, choosing a hook, or checking a draft before publishing.", + "tags": [ + "linkedin", + "copywriting", + "hooks", + "content-drafting", + "pre-publish" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "author": "borghei", + "license": "MIT + Commons Clause", + "category": "tools", + "subdomain": "", + "path": "tools/linkedin/linkedin-post-writer", + "files": [ + "SKILL.md", + "assets/post_brief_template.md", + "assets/sample_brief.json", + "assets/sample_draft.txt", + "assets/sample_draft_revised.txt", + "references/opening-patterns.md", + "references/post-structures.md", + "references/pre-publish-gate.md", + "scripts/angle_picker.py", + "scripts/post_gate.py" + ], + "size_bytes": 84791, + "tools_count": 2, + "has_scripts": true, + "has_references": true, + "has_assets": true + }, + { + "name": "linkedin-profile-optimizer", + "domain": "tools", + "description": "Audits and rewrites a LinkedIn profile from pasted text: headline, About, experience, skills, Featured, banner and photo. Use when reviewing a profile, fixing a headline or About, or preparing a profile before posting more.", + "tags": [ + "linkedin", + "profile", + "headline", + "personal-branding", + "career" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "author": "borghei", + "license": "MIT + Commons Clause", + "category": "tools", + "subdomain": "", + "path": "tools/linkedin/linkedin-profile-optimizer", + "files": [ + "SKILL.md", + "assets/profile_rewrite_worksheet.md", + "assets/profile_template.json", + "assets/sample_profile.json", + "assets/sample_profile_rewritten.json", + "references/experience-skills-and-credibility.md", + "references/featured-and-visuals.md", + "references/headline-and-about.md", + "scripts/profile_auditor.py", + "scripts/profile_rules.py" + ], + "size_bytes": 89035, + "tools_count": 2, + "has_scripts": true, + "has_references": true, + "has_assets": true + }, + { + "name": "linkedin-reply-manager", + "domain": "tools", + "description": "Triages a pasted LinkedIn comment section and drafts replies: which comments to answer, in what order, which to ignore. Use when answering comments on your own post, replying inside a thread, or clearing a backlog.", + "tags": [ + "linkedin", + "replies", + "comment-triage", + "community-management", + "engagement" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "author": "borghei", + "license": "MIT + Commons Clause", + "category": "tools", + "subdomain": "", + "path": "tools/linkedin/linkedin-reply-manager", + "files": [ + "SKILL.md", + "assets/reply_sweep_template.md", + "assets/sample_comment_section.json", + "assets/sample_reply_drafts.json", + "assets/sample_story_bank.json", + "references/difficult-comments.md", + "references/reply-patterns.md", + "references/triage-rules.md", + "scripts/comment_triage.py", + "scripts/reply_linter.py" + ], + "size_bytes": 89594, + "tools_count": 2, + "has_scripts": true, + "has_references": true, + "has_assets": true + }, + { + "name": "linkedin-story-interviewer", + "domain": "tools", + "description": "Interviews a person to surface what they actually have to say, and keeps the answers in a local story bank file that later LinkedIn drafts draw on. Use when someone has never posted, when drafts keep coming out generic, or when the story bank is thin or stale.", + "tags": [ + "linkedin", + "interviewing", + "story-bank", + "content-discovery", + "personal-brand" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "author": "borghei", + "license": "MIT + Commons Clause", + "category": "tools", + "subdomain": "", + "path": "tools/linkedin/linkedin-story-interviewer", + "files": [ + "SKILL.md", + "assets/draft_seed_template.md", + "assets/interview_session_template.md", + "assets/sample_story_bank.json", + "assets/story_bank_template.json", + "references/interview-method.md", + "references/question-sets.md", + "references/story-bank-format.md", + "scripts/story_bank_audit.py", + "scripts/story_bank_rules.py" + ], + "size_bytes": 82324, + "tools_count": 2, + "has_scripts": true, + "has_references": true, + "has_assets": true + }, + { + "name": "linkedin-thread-tracker", + "domain": "tools", + "description": "Keeps a local log of LinkedIn comments you left, records who replied, and reports follow-ups due, overdue, or dead. Use when logging a comment, checking which threads need an answer, or reviewing reply rates.", + "tags": [ + "linkedin", + "follow-up", + "thread-tracking", + "engagement", + "relationship-building" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "author": "borghei", + "license": "MIT + Commons Clause", + "category": "tools", + "subdomain": "", + "path": "tools/linkedin/linkedin-thread-tracker", + "files": [ + "SKILL.md", + "assets/sample_thread_log.json", + "assets/thread_log_template.json", + "assets/weekly_review_template.md", + "references/follow-up-playbook.md", + "references/log-format.md", + "references/thread-states.md", + "scripts/thread_log.py", + "scripts/thread_tracker.py" + ], + "size_bytes": 78340, + "tools_count": 2, + "has_scripts": true, + "has_references": true, + "has_assets": true + }, { "name": "handoff", "domain": "workflow", diff --git a/docs/INSTALLATION.md b/docs/INSTALLATION.md index 3b82e809..c015677f 100644 --- a/docs/INSTALLATION.md +++ b/docs/INSTALLATION.md @@ -1,6 +1,6 @@ # Installation Guide - AI Skills Library -Complete installation guide for all 372 production-ready skills across 20 domains, 867 Python tools, 76 agents, 26 slash commands, 21 compound sub-skills, 18 compliance frameworks and 8 CI/CD workflows. Works with Claude Code, Claude.ai, Cursor, Copilot, Codex, Gemini CLI, Windsurf, Cline, Aider, Goose, ChatGPT and more. +Complete installation guide for all 385 production-ready skills across 21 domains, 900 Python tools, 77 agents, 26 slash commands, 21 compound sub-skills, 18 compliance frameworks and 8 CI/CD workflows. Works with Claude Code, Claude.ai, Cursor, Copilot, Codex, Gemini CLI, Windsurf, Cline, Aider, Goose, ChatGPT and more. ## Table of Contents @@ -122,7 +122,7 @@ Install **one skill per domain group** into your project with built-in auto-upda ### List Available Skills ```bash -# List all 372 skills across 20 domains +# List all 385 skills across 21 domains python scripts/skill-installer.py list # List skills in a specific group @@ -226,6 +226,7 @@ This adds the skills library to your available marketplaces. /plugin install research-skills@claude-code-skills # 4 research skills /plugin install research-ops-skills@claude-code-skills # 4 research ops skills /plugin install markdown-html-skills@claude-code-skills # 4 markdown→HTML skills +/plugin install tools-skills@claude-code-skills # 12 LinkedIn suite skills /plugin install finance-skills@claude-code-skills # 3 finance skills /plugin install workflow-skills@claude-code-skills # 2 workflow meta-skills ``` @@ -962,6 +963,6 @@ See `.codex/skills-index.json` for the complete manifest with descriptions. --- -**Last Updated:** September 2026 -**Skills Version:** 4.12.3 (372 production skills, 867 Python tools, 76 agents, 26 slash commands, 21 compound sub-skills, 8 CI/CD workflows) +**Last Updated:** October 2026 +**Skills Version:** 4.13.0 (385 production skills, 900 Python tools, 77 agents, 26 slash commands, 21 compound sub-skills, 8 CI/CD workflows) **Universal Installer:** [Agent Skills CLI](https://github.com/Karanjot786/agent-skills-cli) diff --git a/docs/agents-reference.md b/docs/agents-reference.md index 2d95a361..4562e449 100644 --- a/docs/agents-reference.md +++ b/docs/agents-reference.md @@ -23,7 +23,7 @@ Located in `.claude/agents/`. Invoke with `/agents/name` in Claude Code. Located in `agents/`. These are specialized AI personas that orchestrate multiple skills with domain-specific behavior. Each agent file defines the persona, tools, and workflows the agent uses. -### Engineering (6) +### Engineering (7) | Agent | Role | File | |-------|------|------| @@ -33,6 +33,7 @@ Located in `agents/`. These are specialized AI personas that orchestrate multipl | **cs-code-auditor** | Deep code audit, quality analysis, refactoring recommendations | `agents/engineering/cs-code-auditor.md` | | **cs-doc-writer** | Technical documentation, API docs, architecture guides | `agents/engineering/cs-doc-writer.md` | | **cs-security-engineer** | Security engineering, threat modeling, vulnerability assessment | `agents/engineering/cs-security-engineer.md` | +| **cs-security-auditor** | Multi-phase source security audit with independent verification; breadth tier chosen per run | `agents/engineering/cs-security-auditor.md` | ### C-Level (3) diff --git a/docs/agents/task-agents.md b/docs/agents/task-agents.md index 850e763c..e336d503 100644 --- a/docs/agents/task-agents.md +++ b/docs/agents/task-agents.md @@ -15,6 +15,7 @@ title: Task Agents | **cs-code-auditor** | Deep code quality and security audits | code-reviewer, senior-security, tech-debt-tracker | | **cs-architecture-reviewer** | System design review and ADR generation | senior-architect, database-designer, migration-architect | | **cs-security-engineer** | Application security assessment and remediation | senior-security, senior-secops, dependency-auditor | +| **cs-security-auditor** | Multi-phase source security audit with verified findings | security-audit | | **cs-doc-writer** | Technical documentation generation | codebase-onboarding, doc-drift-detector, changelog-generator | ## C-Level Agents diff --git a/docs/changelog.md b/docs/changelog.md index 8eafaa6e..9b75d4c5 100644 --- a/docs/changelog.md +++ b/docs/changelog.md @@ -8,6 +8,12 @@ For the complete version history and release notes, see [CHANGELOG.md](https://g ## Current Version +**v4.13.0** (October 2026) + +- New `tools/` domain with the 12-skill LinkedIn suite: story interviews, planning, post writing, hook analysis, humanizing, repurposing, comments, replies, thread tracking, profile, employee advocacy and engagement analytics. Offline tools; nothing is posted for you +- New `security-audit` engineering skill and `cs-security-auditor` agent: multi-agent source audit with a coverage ledger, independent verification and machine-validated findings, at a breadth tier chosen per run +- 385 production-ready skills across 21 domains, 900 Python automation tools, 77 agents + **v4.12.3** (September 2026) - Every script a skill, domain guide or agent tells you to run now exists; references to tools that were never written are gone diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 9adcf37e..d3fc4507 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -13,7 +13,7 @@ git clone https://github.com/borghei/Claude-Skills.git cd Claude-Skills ``` -This gives you all 372 skills, 867 tools, and 76 agents in one checkout. +This gives you all 385 skills, 900 tools, and 77 agents in one checkout. ## Option B: Skill Installer CLI diff --git a/docs/index.md b/docs/index.md index 385b273a..48b890e5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,19 +10,19 @@ Production-ready skill packages that bundle domain expertise, best practices, an
-
372
+
385
Skills
-
867
+
900
Python Tools
-
20
+
21
Domains
-
76
+
77
Agents
@@ -165,6 +165,15 @@ Self-contained HTML documents and slide decks from markdown, a pre-publication r [:octicons-arrow-right-24: Browse skills](skills/markdown-html.md)
+
+### :material-tools: Tools — LinkedIn +**12 skills** · 27 tools · *new domain* + +The LinkedIn suite: story interviews, planning, post writing, humanizing, comments and replies, profile, employee advocacy, and engagement analysis. Offline tools, nothing is posted for you. + +[:octicons-arrow-right-24: Browse skills](skills/tools.md) +
+
--- @@ -173,8 +182,8 @@ Self-contained HTML documents and slide decks from markdown, a pre-publication r | | | |---|---| -| **372 Skills** | Production-ready expertise across 20 professional domains. Project Management is the most-used (70 skills). | -| **867 Python Tools** | CLI scripts for code quality, SEO, DCF valuation, compliance auditing, flow metrics, capacity planning, statistical testing, clinical study sizing -- all standard library, no ML dependencies | +| **385 Skills** | Production-ready expertise across 21 professional domains. Project Management is the most-used (70 skills). | +| **900 Python Tools** | CLI scripts for code quality, SEO, DCF valuation, compliance auditing, flow metrics, capacity planning, statistical testing, clinical study sizing -- all standard library, no ML dependencies | | **25 Role-Based Agents** | Specialized AI personas (Tech Lead, CFO, CISO, Compliance Auditor, etc.) that orchestrate multiple skills | | **6 Subagents** | Autonomous Claude Code agents for code review, security audit, QA, docs, changelog, and git workflows | | **12 CI/CD Workflows** | Ready-to-use GitHub Actions for quality gates, release drafting, skill validation | diff --git a/docs/reference/architecture.md b/docs/reference/architecture.md index 1bb45b2f..048b4dcf 100644 --- a/docs/reference/architecture.md +++ b/docs/reference/architecture.md @@ -77,7 +77,7 @@ Personas combine skills from multiple domains. They define a professional identi | File | Purpose | |---|---| -| `skills.json` | Machine-readable catalog of all 372 skills with metadata | +| `skills.json` | Machine-readable catalog of all 385 skills with metadata | | `CHANGELOG.md` | Version history and release notes | | `CONTRIBUTING.md` | Contribution guidelines | | `LICENSE` | MIT + Commons Clause license | diff --git a/docs/skills-reference.md b/docs/skills-reference.md index 4ff1bfec..ee1d3fda 100644 --- a/docs/skills-reference.md +++ b/docs/skills-reference.md @@ -1,6 +1,6 @@ # Skills Reference -Complete reference for **372 skills across 20 domains**. Each skill is a self-contained package with documentation (`SKILL.md`), Python CLI tools (`scripts/`), knowledge bases (`references/`), and user templates (`assets/`). +Complete reference for **385 skills across 21 domains**. Each skill is a self-contained package with documentation (`SKILL.md`), Python CLI tools (`scripts/`), knowledge bases (`references/`), and user templates (`assets/`). > **Note:** The per-domain tables below are the canonical listing and are generated against `cli/skills.json`. The dated *Additions* sections that follow are a historical changelog — the skills they mention also appear in their domain table. @@ -221,10 +221,11 @@ Enterprise-grade skills with sophisticated analysis tooling. | [chaos-engineering](https://github.com/borghei/Claude-Skills/blob/main/engineering/chaos-engineering/SKILL.md) | Hypothesis-driven fault injection, gameday playbooks, maturity model | 3 | | [secrets-vault-manager](https://github.com/borghei/Claude-Skills/blob/main/engineering/secrets-vault-manager/SKILL.md) | Vault configuration, secret rotation planning, audit log analysis | 3 | -### Security & Threat (4) +### Security & Threat (5) | Skill | Description | Tools | |-------|-------------|-------| +| [security-audit](https://github.com/borghei/Claude-Skills/blob/main/engineering/security-audit/SKILL.md) | Multi-agent source audit: coverage ledger, independent verification, validated findings | 6 | | [cloud-security](https://github.com/borghei/Claude-Skills/blob/main/engineering/cloud-security/SKILL.md) | AWS/Azure/GCP posture: IAM least privilege, exposure, encryption, logging | 4 | | [ai-security](https://github.com/borghei/Claude-Skills/blob/main/engineering/ai-security/SKILL.md) | AI/ML pipeline security, prompt injection, data poisoning risk | 1 | | [red-team](https://github.com/borghei/Claude-Skills/blob/main/engineering/red-team/SKILL.md) | Engagement scoping, rules of engagement, adversary simulation planning | 1 | @@ -725,6 +726,25 @@ The markdown → HTML publishing pipeline: authored Markdown becomes a self-cont | [md-review](https://github.com/borghei/Claude-Skills/blob/main/markdown-html/md-review/SKILL.md) | Pre-publication gate: heading structure, link resolution, readability, a11y | 3 | | [design-system](https://github.com/borghei/Claude-Skills/blob/main/markdown-html/design-system/SKILL.md) | Design tokens, light/dark theming, WCAG contrast, one inlinable CSS bundle | 3 | +## Tools (12) + +Platform toolkits. The LinkedIn suite: find what to say, plan, write, check, converse, and learn from who responded. Offline, stdlib-only, no posting or scraping. + +| Skill | Description | Tools | +|-------|-------------|-------| +| [linkedin-story-interviewer](https://github.com/borghei/Claude-Skills/blob/main/tools/linkedin/linkedin-story-interviewer) | Interview that surfaces what you have to say; keeps a local story bank | 2 | +| [linkedin-content-planner](https://github.com/borghei/Claude-Skills/blob/main/tools/linkedin/linkedin-content-planner) | Weekly or monthly plan from pillars, cadence and format mix; founder pillar set | 2 | +| [linkedin-post-writer](https://github.com/borghei/Claude-Skills/blob/main/tools/linkedin/linkedin-post-writer) | Angle and opening pattern from a brief, post structure, pre-publish gate | 2 | +| [linkedin-hook-analyzer](https://github.com/borghei/Claude-Skills/blob/main/tools/linkedin/linkedin-hook-analyzer) | Classifies opening-line patterns in saved posts into reusable templates | 2 | +| [linkedin-humanizer](https://github.com/borghei/Claude-Skills/blob/main/tools/linkedin/linkedin-humanizer) | Tiered machine-writing tell audit, emoji scoring, voice fingerprint | 4 | +| [linkedin-content-repurposer](https://github.com/borghei/Claude-Skills/blob/main/tools/linkedin/linkedin-content-repurposer) | Thread, transcript or article into a native post, with a fidelity check | 3 | +| [linkedin-comment-writer](https://github.com/borghei/Claude-Skills/blob/main/tools/linkedin/linkedin-comment-writer) | Comments that add something, gated by a 13-rule linter | 2 | +| [linkedin-reply-manager](https://github.com/borghei/Claude-Skills/blob/main/tools/linkedin/linkedin-reply-manager) | Triage of a pasted comment section and gated reply drafts | 2 | +| [linkedin-thread-tracker](https://github.com/borghei/Claude-Skills/blob/main/tools/linkedin/linkedin-thread-tracker) | Local log of comments left: replies received, follow-ups due, dead threads | 2 | +| [linkedin-profile-optimizer](https://github.com/borghei/Claude-Skills/blob/main/tools/linkedin/linkedin-profile-optimizer) | Section-by-section profile audit and rewrite | 2 | +| [linkedin-employee-advocacy](https://github.com/borghei/Claude-Skills/blob/main/tools/linkedin/linkedin-employee-advocacy) | Team cadence matrix, consent and review rules for an advocacy programme | 2 | +| [linkedin-engagement-analytics](https://github.com/borghei/Claude-Skills/blob/main/tools/linkedin/linkedin-engagement-analytics) | Audience segments from an engagement export, compared with the target | 2 | + ## Workflow (2) Meta-skills for routing work and handing it off. diff --git a/docs/skills/engineering.md b/docs/skills/engineering.md index 7116e24f..04665baf 100644 --- a/docs/skills/engineering.md +++ b/docs/skills/engineering.md @@ -4,7 +4,7 @@ title: Engineering Skills # Engineering Skills -**82 skills** with **248+ Python tools** covering fullstack development, DevOps (incl. chaos engineering, feature flags, Kubernetes operators), security, mobile, ML/AI, cloud architecture (AWS / Azure / GCP), data quality, and developer tooling. +**92 skills** with **254+ Python tools** covering fullstack development, DevOps (incl. chaos engineering, feature flags, Kubernetes operators), security, mobile, ML/AI, cloud architecture (AWS / Azure / GCP), data quality, and developer tooling. ## AI & Agent Systems @@ -78,6 +78,7 @@ title: Engineering Skills | [skill-security-auditor](https://github.com/borghei/Claude-Skills/blob/main/engineering/skill-security-auditor/SKILL.md) | Security audit for AI agent skills -- prompt injection, dangerous code patterns | 3 | | [threat-detection](https://github.com/borghei/Claude-Skills/blob/main/engineering/threat-detection/SKILL.md) | Log analysis for brute force, injection attempts, privilege escalation | 1 | | [red-team](https://github.com/borghei/Claude-Skills/blob/main/engineering/red-team/SKILL.md) | Security engagement planning, rules of engagement, attack phase planning | 1 | +| [security-audit](https://github.com/borghei/Claude-Skills/blob/main/engineering/security-audit/SKILL.md) | Multi-agent source security audit -- coverage ledger, independent verification, machine-validated findings, three breadth tiers | 6 | | [dependency-auditor](https://github.com/borghei/Claude-Skills/blob/main/engineering/dependency-auditor/SKILL.md) | Dependency vulnerabilities, license compliance, upgrade planning | 3 | ## Quality & Testing diff --git a/docs/skills/index.md b/docs/skills/index.md index 7313be33..9f1cbdf9 100644 --- a/docs/skills/index.md +++ b/docs/skills/index.md @@ -4,14 +4,14 @@ title: Skill Catalog # Skill Catalog -372 production-ready skills across 20 domains, each with Python CLI tools, reference guides, and templates. +385 production-ready skills across 21 domains, each with Python CLI tools, reference guides, and templates. ## Domains at a Glance | Domain | Skills | Tools | Page | |---|:---:|:---:|---| | **★ Project Management** (most-used) | **70** | **82+** | **[View all](project-management.md)** | -| Engineering | 91 | 248 | [View all](engineering.md) | +| Engineering | 92 | 254 | [View all](engineering.md) | | Marketing | 41 | 118+ | [View all](marketing.md) | | C-Level Advisory | 31 | 88+ | [View all](c-level.md) | | RA/QM & Compliance | 27 | 50+ | [View all](compliance.md) | @@ -28,9 +28,10 @@ title: Skill Catalog | Business Operations (new) | 6 | 18 | [View all](business-operations.md) | | Research Ops (new) | 4 | 12 | [View all](research-ops.md) | | Markdown & HTML (new) | 4 | 14 | [View all](markdown-html.md) | +| Tools — LinkedIn suite (new) | 12 | 27 | [View all](tools.md) | | Finance | 3 | 10 | [View all](business.md#finance) | | Workflow (meta-skills) | 2 | — | [View all](other.md) | -| **Total** | **372** | **867** | | +| **Total** | **385** | **900** | | ## Skill Package Structure diff --git a/docs/skills/tools.md b/docs/skills/tools.md new file mode 100644 index 00000000..6c3d9e41 --- /dev/null +++ b/docs/skills/tools.md @@ -0,0 +1,125 @@ +--- +title: Tools — LinkedIn Suite +description: Platform toolkits. The LinkedIn suite covers finding what to say, planning, writing, humanizing, commenting, replying, profile, team advocacy and engagement analysis. Offline, stdlib-only tools. +--- + +# Tools + +**12 skills** with **27 stdlib-only Python tools**. Platform toolkits live here; the first is the **LinkedIn suite**. + +Where [Marketing](marketing.md) covers channel strategy, these skills do the hands-on work on one platform: decide what to say, write it, check it, publish it yourself, and learn from who responded. + +!!! info "New domain (October 2026)" + **Offline by design.** No skill here posts, schedules, or scrapes. Each produces drafts and plans you paste into LinkedIn yourself, and analyses text or exports you provide. Scripts use the standard library only and make no network calls. Several tools are gates that exit non-zero on their flawed sample input; each `SKILL.md` documents its exit codes. + +## LinkedIn suite + +### linkedin-story-interviewer + +[:material-folder-open: Browse on GitHub](https://github.com/borghei/Claude-Skills/tree/main/tools/linkedin/linkedin-story-interviewer){ .md-button } + +Interviews a person to surface what they actually have to say, and keeps the answers in a local story bank file that later LinkedIn drafts draw on. Use when someone has never posted, when drafts keep coming out generic, or when the story bank is thin or stale. + +**Tools:** `story_bank_audit.py`, `story_bank_rules.py` + +### linkedin-content-planner + +[:material-folder-open: Browse on GitHub](https://github.com/borghei/Claude-Skills/tree/main/tools/linkedin/linkedin-content-planner){ .md-button } + +Builds a weekly or monthly LinkedIn publishing plan from content pillars, a posting cadence and a format mix, with a founder-oriented pillar set as an alternative to the general one. Use when planning a week, a month or a launch period of posts rather than drafting a single post. + +**Tools:** `plan_builder.py`, `plan_rules.py` + +### linkedin-post-writer + +[:material-folder-open: Browse on GitHub](https://github.com/borghei/Claude-Skills/tree/main/tools/linkedin/linkedin-post-writer){ .md-button } + +Drafts LinkedIn posts from a brief: picks an angle and opening pattern the author's real material supports, structures the body, and gates the draft before it is pasted. Use when writing a post, choosing a hook, or checking a draft before publishing. + +**Tools:** `angle_picker.py`, `post_gate.py` + +### linkedin-hook-analyzer + +[:material-folder-open: Browse on GitHub](https://github.com/borghei/Claude-Skills/tree/main/tools/linkedin/linkedin-hook-analyzer){ .md-button } + +Extracts and classifies the opening-line pattern of LinkedIn posts the user pastes or saves, and turns each into a reusable slot template with cautions. Use when studying why an opening works, building a swipe file, or adapting a hook pattern without copying it. + +**Tools:** `hook_classifier.py`, `hook_patterns.py` + +### linkedin-humanizer + +[:material-folder-open: Browse on GitHub](https://github.com/borghei/Claude-Skills/tree/main/tools/linkedin/linkedin-humanizer){ .md-button } + +Audits and rewrites LinkedIn drafts to remove machine-sounding patterns: tiered tell catalogue, emoji-pattern scoring, rule explanations, and a voice fingerprint built from the author's own posts. Use when a draft reads generated, before publishing, or when an edit flattened someone's voice. + +**Tools:** `emoji_audit.py`, `tell_audit.py`, `tell_rules.py`, `voice_fingerprint.py` + +### linkedin-content-repurposer + +[:material-folder-open: Browse on GitHub](https://github.com/borghei/Claude-Skills/tree/main/tools/linkedin/linkedin-content-repurposer){ .md-button } + +Turns something made for another channel (a thread, a video or talk transcript, a blog post, a newsletter) into a post that reads as native to LinkedIn, without changing what the source says. Use when existing material should become a LinkedIn post rather than writing one from a blank page. + +**Tools:** `draft_fidelity_check.py`, `repurpose_analyzer.py`, `repurpose_rules.py` + +### linkedin-comment-writer + +[:material-folder-open: Browse on GitHub](https://github.com/borghei/Claude-Skills/tree/main/tools/linkedin/linkedin-comment-writer){ .md-button } + +Drafts comments on other people's LinkedIn posts that add a specific the post lacked, then gates them offline. Use when commenting on a post, engaging with a prospect or peer, or checking a comment before pasting it. + +**Tools:** `comment_linter.py`, `comment_rules.py` + +### linkedin-reply-manager + +[:material-folder-open: Browse on GitHub](https://github.com/borghei/Claude-Skills/tree/main/tools/linkedin/linkedin-reply-manager){ .md-button } + +Triages a pasted LinkedIn comment section and drafts replies: which comments to answer, in what order, which to ignore. Use when answering comments on your own post, replying inside a thread, or clearing a backlog. + +**Tools:** `comment_triage.py`, `reply_linter.py` + +### linkedin-thread-tracker + +[:material-folder-open: Browse on GitHub](https://github.com/borghei/Claude-Skills/tree/main/tools/linkedin/linkedin-thread-tracker){ .md-button } + +Keeps a local log of LinkedIn comments you left, records who replied, and reports follow-ups due, overdue, or dead. Use when logging a comment, checking which threads need an answer, or reviewing reply rates. + +**Tools:** `thread_log.py`, `thread_tracker.py` + +### linkedin-profile-optimizer + +[:material-folder-open: Browse on GitHub](https://github.com/borghei/Claude-Skills/tree/main/tools/linkedin/linkedin-profile-optimizer){ .md-button } + +Audits and rewrites a LinkedIn profile from pasted text: headline, About, experience, skills, Featured, banner and photo. Use when reviewing a profile, fixing a headline or About, or preparing a profile before posting more. + +**Tools:** `profile_auditor.py`, `profile_rules.py` + +### linkedin-employee-advocacy + +[:material-folder-open: Browse on GitHub](https://github.com/borghei/Claude-Skills/tree/main/tools/linkedin/linkedin-employee-advocacy){ .md-button } + +Designs and runs an employee advocacy programme: who posts what and how often, review rules, and what is never scripted. Use when getting a team posting, writing an advocacy policy, or fixing a programme that has stalled. + +**Tools:** `advocacy_planner.py`, `advocacy_rules.py` + +### linkedin-engagement-analytics + +[:material-folder-open: Browse on GitHub](https://github.com/borghei/Claude-Skills/tree/main/tools/linkedin/linkedin-engagement-analytics){ .md-button } + +Segments who reacted to and commented on a post from a CSV or JSON export, and says whether it reached the intended audience. Use when reviewing a post's engagers, checking audience fit, or building a baseline. + +**Tools:** `engager_segmenter.py`, `segment_rules.py` + +## How the suite fits together + +| Stage | Skill | +|---|---| +| Find material | `linkedin-story-interviewer` | +| Plan | `linkedin-content-planner` | +| Write | `linkedin-post-writer`, `linkedin-hook-analyzer`, `linkedin-content-repurposer` | +| Check | `linkedin-humanizer` | +| Converse | `linkedin-comment-writer`, `linkedin-reply-manager`, `linkedin-thread-tracker` | +| Presence | `linkedin-profile-optimizer`, `linkedin-employee-advocacy` | +| Learn | `linkedin-engagement-analytics` | + +Each skill is self-contained and installs alone. Where one accepts another's output (the story bank file), that input is optional. diff --git a/documentation/WORKFLOW.md b/documentation/WORKFLOW.md index 3a3ddcfc..bb4a0da7 100644 --- a/documentation/WORKFLOW.md +++ b/documentation/WORKFLOW.md @@ -127,8 +127,8 @@ There is no `dev → main` release PR. A release is simply an annotated tag on ` ```bash git checkout main && git pull origin main -git tag -a v4.12.3 -m "v4.12.3 — " -git push origin v4.12.3 +git tag -a v4.13.0 -m "v4.13.0 — " +git push origin v4.13.0 ``` (`cli-v*` tags trigger the npm publish workflow; `vX.Y.Z` tags do not.) diff --git a/engineering/.claude-plugin/plugin.json b/engineering/.claude-plugin/plugin.json index 6793f3f3..1861631f 100644 --- a/engineering/.claude-plugin/plugin.json +++ b/engineering/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "engineering-advanced-skills", - "description": "82 engineering skills: architecture, fullstack, DevOps, security, AI/ML, data engineering, and agent tooling", + "description": "92 engineering skills: architecture, fullstack, DevOps, security, AI/ML, data engineering, and agent tooling", "version": "1.0.0", "author": { "name": "borghei", diff --git a/engineering/security-audit/SKILL.md b/engineering/security-audit/SKILL.md new file mode 100644 index 00000000..5149348b --- /dev/null +++ b/engineering/security-audit/SKILL.md @@ -0,0 +1,278 @@ +--- +name: security-audit +description: > + Multi-agent source security audit with a coverage ledger, independently + verified findings, and machine-checked records. Use when asked to audit a + codebase, find vulnerabilities, or run a source-level pen test. +license: MIT + Commons Clause +metadata: + version: 1.0.0 + author: borghei + category: engineering + domain: application-security + updated: 2026-10-07 + tags: [security-audit, vulnerability-research, multi-agent, code-review, appsec] +--- + +# Security Audit + +A single pass over a codebase finds the bugs that pass happened to look at and +reports them with whatever confidence the reader had that day. This skill +replaces that with a process: a lead agent maps the target, writes down exactly +what will be examined, sends isolated hunters at each piece, has a different +agent try to disprove every claim, and only then writes a report — from records +that two validators have checked. + +The output is a set of findings in three honest states: **confirmed** (traced +through source and reproduced locally), **needs validation** (a specific +hypothesis blocked on one named fact), and **rejected** (disproved, kept so it +is not raised again). + +**Scope boundary.** This is a defensive, source-first audit of a repository. It +does **not** send traffic to deployed systems — that needs an engagement plan +from `red-team`. It does **not** produce a STRIDE threat model or scan for +secrets as its main job — that is `senior-security`. It does **not** query live +cloud accounts — that is `cloud-security`. It does **not** list dependency CVEs +— that is `dependency-auditor`. What lives here is finding, verifying, and +documenting boundary failures in the code itself. + +## When to use this skill + +- A request for a security audit, a vulnerability hunt, or a source-level pen test of a repository or one of its directories +- A release, acquisition, or customer review needs findings that stand up to scrutiny +- A previous audit produced a long list nobody trusts, and the real ones must be separated out +- A security-relevant change needs a scoped review of one subsystem or one diff +- A repeat audit should target what the last one deferred, blocked, or left out + +## Two modes + +- **Guidance mode** — a security question, a focused review of one file or + finding, or triage. Use the relevant reference, answer in the conversation, + create no files and no run directory. +- **Full audit mode** — the user explicitly wants the repository audited or + pen-tested, or wants report files. Run all six phases. + +If the request could be either, ask which before creating anything. + +## Clarify First + +Before a full audit, confirm these. Ask — do not assume: + +- [ ] **Breadth tier** — decides which attack-class files are in play, and therefore cost and coverage. Offer all three: + + | Tier | Covers | Fits | + |---|---|---| + | `core` | The 9 core classes only | A first look, a small service, a tight budget | + | `focused` | Core + web/auth, AI/LLM, supply chain, cloud | Most web and API products; the default | + | `full` | Core + all 10 companions | High-stakes targets, native code, multi-tenant platforms, device apps | + +- [ ] **Target and scope** — the repository root, and whether to audit all of it, named paths, or a diff between two refs +- [ ] **Output location** — a directory outside the target (default `~/security-audits//run-`) +- [ ] **Profile and budget** — `quick`, `standard`, or `deep`, and any cap on agent invocations + +Stop rule: always ask the tier; ask the others only if the request leaves them +open. If the user says "just run it", use `focused`, `standard`, whole +repository, default output, no budget cap — and state those choices at the top +of the report. + +A tier limits which companions are eligible. A boundary whose companion is +outside the tier is recorded as an `out_of_scope` unit and named in the report. +It is never silently skipped, and never counted as covered. + +## Execution safety + +These rules hold in both modes and are not negotiable by profile or budget. + +- Reading source is always allowed. **Running target code** — builds, tests, + fixtures, fuzzers, browsers, emulators — is allowed only inside an + OS-enforced sandbox with no external network, an environment built from an + explicit allowlist, a read-only target, writes confined to the agent's + `scratch/`, and CPU, memory, process, file-size, disk, and time limits. +- If any of those controls is unavailable, nothing is executed. The lead is + recorded as `needs_validation` with the missing control as its blocker. +- No deployed endpoints, external services, real accounts, real credentials, + production data, shared queues, registries, or paid APIs. No dependency + installation. No stress or volume testing. +- A local check stops at the first result that settles the question. +- The audit never edits the target repository. It describes fixes. +- Sandbox output is target-controlled. Only `promote_evidence.py` moves a file + from `scratch/` to `evidence/`. + +## The six phases + +| # | Phase | Who | Output | Reference | +|---|---|---|---|---| +| 1 | Reconnaissance | 4 read-only scouts, then the lead | `architecture.md`, seeded `coverage-ledger.json` | `references/reconnaissance.md` | +| 2 | Coverage-led hunting | Hunters per unit, a critic per wave | Updated ledger, candidates | `references/hunting.md`, attack-class files | +| 3 | Candidate validation | One fresh verifier per candidate | A decided record per fingerprint | `references/validation-and-reporting.md` | +| 4 | Structured output | The lead | `findings.json`, both validators pass | same | +| 5 | Record verification | One fresh reader per record | Verified or independently re-checked records | same | +| 6 | Reporting | The lead | `REPORT.md`, `FINDINGS-DETAIL.md`, `NEEDS-VALIDATION.md` | same, `assets/report_template.md` | + +Three rules of independence hold the process together: the lead is the only +writer of shared files; no agent validates a candidate it hunted; and no agent +sees another verifier's conclusion. + +A run ends in exactly one of two states: every Phase 6 file is written and both +validators pass, or `run_status` is `incomplete` with the exact reason and the +gap is stated in the report's first paragraph. + +## Workflows + +### Workflow 1 — Full audit + +1. Ask the tier, then create the run with `init`. +2. Launch the four scouts, write `architecture.md`, and seed the ledger — every + unit ID derived with `coverage-id`. Validate the ledger. +3. Assign hunters wave by wave from `planned` units; update and re-validate the + ledger after each result; run the coverage critic after each wave. +4. Promote any local-check output with `promote_evidence.py` before citing it. +5. Give each candidate to a fresh verifier, write `findings.json`, and run both + validators. +6. Give each retained record to a fresh reader; re-validate after any change. +7. Pass the `--final` gate, paste the `summary` tables, and write the reports. + +```bash +# 1. Create the run outside the target (asks nothing; pass what the user chose) +python3 engineering/security-audit/scripts/audit_run.py init \ + --target ~/code/app --tier focused --profile standard + +# 2. After seeding units from reconnaissance, and after every ledger edit +python3 engineering/security-audit/scripts/validate_coverage_ledger.py \ + /coverage-ledger.json --metadata /run-metadata.json + +# 3. After a hunter or verifier ran a local check +python3 engineering/security-audit/scripts/promote_evidence.py \ + --run-dir --agent-id hunter-03 --file result.txt + +# 4. After writing findings.json, and after every replacement +python3 engineering/security-audit/scripts/validate_findings.py /findings.json + +# 5. Final gate, then the coverage tables for REPORT.md +python3 engineering/security-audit/scripts/validate_coverage_ledger.py \ + /coverage-ledger.json --metadata /run-metadata.json \ + --findings /findings.json --run-dir --final +python3 engineering/security-audit/scripts/audit_run.py summary --run-dir +``` + +### Workflow 2 — Scoped or budgeted audit + +```bash +python3 engineering/security-audit/scripts/audit_run.py init \ + --target ~/code/app --tier core --profile quick \ + --scope services/billing --budget 14 + +python3 engineering/security-audit/scripts/audit_run.py budget \ + --budget 14 --profile quick --units 9 +``` + +`init` refuses a budget that cannot pay for reconnaissance, the critic, and one +verifier. `budget` reserves those first and reports how many units must be +`deferred`. A scoped, `quick`, or budget-limited run presents itself as a +partial pass. + +### Workflow 3 — Repeat audit + +Run `init` again on the same target. It lists prior runs; follow "Using prior +runs" in `references/reconnaissance.md` to carry unchanged confirmed records to +a fresh verifier, reopen every deferred, blocked, and out-of-scope unit, and +revalidate anything whose source changed. Repeat runs are how coverage grows — +plan for at least two on anything that matters. + +## Decision frameworks + +### Choosing a profile + +| Profile | Units | Hunting loop | Review per candidate | Use when | +|---|---|---|---|---| +| `quick` | One per surface × boundary × class | One wave, one critic | One fresh verifier | [RECOMMENDED] Small targets, re-runs, a first look | +| `standard` | Split by subsystem | Waves until two critics are clean | Validator, then a separate record reader | [PROVEN] The default | +| `deep` | Split by subsystem and lifecycle mode | As standard, plus a second pass over re-checked units | As standard | [RECOMMENDED] High stakes or large targets | + +Profiles change breadth and redundancy. They never lower the evidence bar. + +### Severity calibration + +| Severity | Anchor | +|---|---| +| **Critical** | Anyone on the network, with no account, can run code, read or rewrite the whole data store, or take over any user they choose | +| **High** | A named control is completely bypassed and it matters: signing in without credentials, one tenant reading or changing another's data, script planted for other users to run, a logged-in user running code, an anonymous request that halts a service others depend on | +| **Medium** | The boundary is genuinely crossed, but reach is small, the setup is unusual, or only a few resources are exposed | +| **Low** | Internal detail that is not secret leaks, or the attacker works hard for very little | +| **Informational** | True but nearly harmless alone; worth recording because another finding builds on it | + +Only `confirmed` records get a severity, and `overall` cannot exceed the impact +that was demonstrated. A rating you cannot back with a one-sentence description +of the harm is too high. + +### Confirmed, or needs validation? + +| Situation | Verdict | +|---|---| +| Full trace in source and a bounded local result observed | `confirmed` | +| Full trace, but the result depends on a proxy, provider, or policy not in the repository | `needs_validation`, blocker named | +| Full trace, but no sandbox to run the check | `needs_validation`, missing control named | +| A layer visible in source prevents it | `rejected` | +| Good practice missing, nobody harmed | Hardening note — not a record | + +## Anti-Patterns + +### The checklist finding +**Mistake:** Reporting "missing rate limiting" or "cookie lacks a flag" as a vulnerability. +**Why it happens:** Deviations from a list are quick to spot and look like output. +**Instead:** Require an actor, a boundary, and a result. No victim means a hardening note. + +### Grading your own homework +**Mistake:** The agent that found a candidate also confirms it, or a verifier is shown the previous verdict. +**Why it happens:** It saves an agent, and the finder already has the context loaded. +**Instead:** Every candidate goes to an agent that did not hunt it and is told to refute it. Refutation is the step that removes false positives; skipping it moves that work to whoever reads the report. + +### Guessing the deployment +**Mistake:** "The load balancer probably strips that header" — or probably does not — decides the verdict. +**Why it happens:** The repository ends where the infrastructure begins, and a confident sentence reads better than an open question. +**Instead:** Record `needs_validation` with the exact fact and a check the owner can make in their own environment. + +### Coverage by assertion +**Mistake:** The report says "authentication and authorization were reviewed" with no record of which paths. +**Why it happens:** Agents ran, tokens were spent, and it feels covered. +**Instead:** The ledger is the claim. A unit closes only with reviewed paths and checks; everything unreached is `deferred` or `out_of_scope` with a reason, and the report lists them. + +### Severity by bug class +**Mistake:** Rating a finding high because it is called injection, or parking a guess as "low confidence, critical". +**Why it happens:** Class names carry reputations, and an alarming rating gets attention. +**Instead:** Rate what the reproduction showed. The findings validator rejects an overall severity above demonstrated impact, and a high or critical record held with low confidence. + +### One run and done +**Mistake:** Treating a single clean pass as proof the code is sound. +**Why it happens:** The report looks finished. +**Instead:** Say what the run did not cover, and schedule a repeat run that starts from those gaps. + +## Files + +| File | Purpose | +|------|---------| +| `scripts/audit_run.py` | `init` a run directory outside the target; derive `coverage-id`s; split a `budget`; print the `summary` tables for the report | +| `scripts/validate_coverage_ledger.py` | Validates the ledger: derived IDs, order, state table, owned evidence, resolvable blocks, tier confinement, findings cross-links, `--final` gate | +| `scripts/validate_findings.py` | Validates `findings.json` against the schema plus ordering, path safety, trace shape, and severity rules | +| `scripts/promote_evidence.py` | The only path from `scratch/` to `evidence/`: no-follow, single-link regular files within byte limits, exclusive create | +| `scripts/ledger_rules.py` | Unit state table, evidence rules, and reopened-attempt rules behind the ledger validator; run it to print the table | +| `scripts/audit_common.py` | Shared vocabularies, tier definitions, ID and path rules; run it to print the contracts | +| `references/reconnaissance.md` | Phase 1: scout prompts, prior-run handling, architecture summary, ledger seeding, unit states | +| `references/hunting.md` | Phase 2: assignment order, hunter prompt, candidate gate, result format, coverage critic, stop rules | +| `references/validation-and-reporting.md` | Phases 3–6: verifier prompt, record fields, material replacements, report contents | +| `references/attack-classes.md` | The 9 core classes and the companion routing table | +| `references/web-protocol-and-auth.md` | Focused tier: HTTP framing, caches, sessions, tokens, OAuth/OIDC, SAML, MFA, passkeys, API keys, mTLS | +| `references/ai-and-llm.md` | Focused tier: indirect injection, memory, tool calls, approvals, MCP, output handling | +| `references/supply-chain-and-release.md` | Focused tier: dependency sources, CI workflows, artifacts, signing, updates, plugins | +| `references/cloud-and-deployment.md` | Focused tier: workload identity, exposure, containers, config precedence, secrets, storage, events | +| `references/client-side.md` | Full tier: DOM sinks, messaging, CORS, service workers, storage, state leaks, framing | +| `references/data-isolation-and-lifecycle.md` | Full tier: tenant scope, derived copies, export and restore, deletion, retention | +| `references/desktop-mobile-and-local-ipc.md` | Full tier: deep links, web-view bridges, local IPC, privileged helpers, device lifecycle | +| `references/memory-safety-and-binary.md` | Full tier: bounds, integers, lifetimes, FFI, loaders, JIT, kernel interfaces | +| `references/protocols-rpc-and-messaging.md` | Full tier: wire formats, RPC identity, brokers, replay and ordering | +| `references/resource-exhaustion-and-availability.md` | Full tier: amplification, accumulation, quotas, starvation, recovery | +| `assets/findings.schema.json` | JSON Schema for the three record verdicts | +| `assets/sample_findings.json` | One record of each verdict; passes the validator | +| `assets/sample_coverage_ledger.json` | Units in each state, including a reopened attempt; passes the validator | +| `assets/sample_run_metadata.json` | Run metadata as `init` writes it | +| `assets/report_template.md` | Layout for `REPORT.md` | diff --git a/engineering/security-audit/assets/findings.schema.json b/engineering/security-audit/assets/findings.schema.json new file mode 100644 index 00000000..dfcafa2a --- /dev/null +++ b/engineering/security-audit/assets/findings.schema.json @@ -0,0 +1,188 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "Security audit findings", + "description": "Final records of one audit run. Each record carries exactly one verdict, and the verdict decides which fields are allowed. Validate with scripts/validate_findings.py, which also enforces ordering, path safety, trace shape, and severity rules that a schema cannot express.", + "type": "object", + "required": ["schema_version", "run_id", "findings"], + "additionalProperties": false, + "properties": { + "schema_version": {"type": "integer", "const": 1}, + "run_id": {"type": "string", "minLength": 1}, + "findings": { + "type": "array", + "items": { + "oneOf": [ + {"$ref": "#/$defs/confirmed"}, + {"$ref": "#/$defs/needs_validation"}, + {"$ref": "#/$defs/rejected"} + ] + } + } + }, + "$defs": { + "fingerprint": { + "type": "string", + "minLength": 1, + "pattern": "^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,199}$" + }, + "text": {"type": "string", "minLength": 1}, + "level": {"type": "string", "enum": ["informational", "low", "medium", "high", "critical"]}, + "trace": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["step", "file", "line", "symbol", "note"], + "additionalProperties": false, + "properties": { + "step": {"type": "string", "enum": ["entry", "flow", "sink"]}, + "file": {"$ref": "#/$defs/text"}, + "line": {"type": "integer", "minimum": 1}, + "symbol": {"$ref": "#/$defs/text"}, + "note": {"$ref": "#/$defs/text"} + } + } + }, + "evidence": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["file", "line", "note"], + "additionalProperties": false, + "properties": { + "file": {"$ref": "#/$defs/text"}, + "line": {"type": "integer", "minimum": 1}, + "note": {"$ref": "#/$defs/text"} + } + } + }, + "rated": { + "type": "object", + "required": ["level", "why"], + "additionalProperties": false, + "properties": { + "level": {"$ref": "#/$defs/level"}, + "why": {"$ref": "#/$defs/text"} + } + }, + "confirmed": { + "type": "object", + "required": ["verdict", "fingerprint", "title", "summary", "root_cause", "violated_invariant", "actor", "affected", "trace", "evidence", "preconditions", "reproduction", "fix", "severity", "confidence"], + "additionalProperties": false, + "properties": { + "verdict": {"type": "string", "const": "confirmed"}, + "fingerprint": {"$ref": "#/$defs/fingerprint"}, + "title": {"$ref": "#/$defs/text"}, + "summary": {"$ref": "#/$defs/text"}, + "root_cause": {"$ref": "#/$defs/text"}, + "violated_invariant": {"$ref": "#/$defs/text"}, + "actor": {"$ref": "#/$defs/text"}, + "affected": {"$ref": "#/$defs/text"}, + "trace": {"$ref": "#/$defs/trace"}, + "evidence": {"$ref": "#/$defs/evidence"}, + "preconditions": { + "type": "array", + "items": { + "type": "object", + "required": ["kind", "detail"], + "additionalProperties": false, + "properties": { + "kind": {"type": "string", "enum": ["authentication", "role", "user_interaction", "configuration", "network_position", "environment", "data_state", "timing", "third_party"]}, + "detail": {"$ref": "#/$defs/text"} + } + } + }, + "reproduction": { + "type": "object", + "required": ["interface", "inputs", "steps", "observed"], + "additionalProperties": false, + "properties": { + "interface": {"$ref": "#/$defs/text"}, + "inputs": {"type": "array", "minItems": 1, "items": {"type": "string"}}, + "steps": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/text"}}, + "observed": {"$ref": "#/$defs/text"} + } + }, + "fix": { + "type": "object", + "required": ["strategy", "regression_test"], + "additionalProperties": false, + "properties": { + "strategy": {"$ref": "#/$defs/text"}, + "regression_test": {"$ref": "#/$defs/text"}, + "changes": { + "type": "array", + "items": { + "type": "object", + "required": ["file", "change"], + "additionalProperties": false, + "properties": { + "file": {"$ref": "#/$defs/text"}, + "change": {"$ref": "#/$defs/text"} + } + } + } + } + }, + "severity": { + "type": "object", + "required": ["likelihood", "impact", "overall"], + "additionalProperties": false, + "properties": { + "likelihood": {"$ref": "#/$defs/rated"}, + "impact": {"$ref": "#/$defs/rated"}, + "overall": {"$ref": "#/$defs/level"} + } + }, + "confidence": { + "type": "object", + "required": ["level", "why"], + "additionalProperties": false, + "properties": { + "level": {"type": "string", "enum": ["low", "medium", "high"]}, + "why": {"$ref": "#/$defs/text"} + } + } + } + }, + "needs_validation": { + "type": "object", + "required": ["verdict", "fingerprint", "title", "summary", "suspected_root_cause", "trace", "evidence", "blockers", "resolution"], + "additionalProperties": false, + "properties": { + "verdict": {"type": "string", "const": "needs_validation"}, + "fingerprint": {"$ref": "#/$defs/fingerprint"}, + "title": {"$ref": "#/$defs/text"}, + "summary": {"$ref": "#/$defs/text"}, + "suspected_root_cause": {"$ref": "#/$defs/text"}, + "trace": {"$ref": "#/$defs/trace"}, + "evidence": {"$ref": "#/$defs/evidence"}, + "blockers": {"type": "array", "minItems": 1, "items": {"$ref": "#/$defs/text"}}, + "resolution": { + "type": "object", + "additionalProperties": false, + "properties": { + "local": {"$ref": "#/$defs/text"}, + "owner_check": {"$ref": "#/$defs/text"} + } + } + } + }, + "rejected": { + "type": "object", + "required": ["verdict", "fingerprint", "title", "summary", "suspected_root_cause", "trace", "evidence", "rejection_reason"], + "additionalProperties": false, + "properties": { + "verdict": {"type": "string", "const": "rejected"}, + "fingerprint": {"$ref": "#/$defs/fingerprint"}, + "title": {"$ref": "#/$defs/text"}, + "summary": {"$ref": "#/$defs/text"}, + "suspected_root_cause": {"$ref": "#/$defs/text"}, + "trace": {"$ref": "#/$defs/trace"}, + "evidence": {"$ref": "#/$defs/evidence"}, + "rejection_reason": {"$ref": "#/$defs/text"} + } + } + } +} diff --git a/engineering/security-audit/assets/report_template.md b/engineering/security-audit/assets/report_template.md new file mode 100644 index 00000000..14a33d8d --- /dev/null +++ b/engineering/security-audit/assets/report_template.md @@ -0,0 +1,88 @@ +# Security Audit Report — {{repo}} + + + +## Run facts + +| | | +|---|---| +| Run | {{run_id}} | +| Source | {{commit}} ({{clean or dirty worktree}}) | +| Breadth tier | {{core / focused / full}} — {{companions in play}} | +| Profile | {{quick / standard / deep}} | +| Scope | {{whole repository, or the listed paths}} | +| Budget | {{agents spent}} of {{budget, or "no cap"}} | +| Prior runs used | {{paths, or "none — this is the first run"}} | +| Evidence | Source review and sandboxed local checks only. No deployed system was contacted. | +| Status | {{complete / incomplete — reason}} | + + + +**This is a partial pass.** {{What limited it, and what that leaves unexamined.}} + +## Posture + +{{One short paragraph: what the code does well, where the confirmed problems +cluster, and what the unresolved leads have in common.}} + +## Confirmed findings + +| Severity | Finding | Boundary | Observed result | +|---|---|---|---| +| {{high}} | {{title}} | {{boundary}} | {{one line from reproduction.observed}} | + +### {{title}} — {{severity}} + +- **Fingerprint:** `{{fingerprint}}` +- **Location:** `{{file}}:{{line}}` ({{symbol}}) +- **Who can do it:** {{actor}} +- **What is affected:** {{affected}} +- **Preconditions:** {{preconditions}} +- **Reproduction (local, dummy data):** {{interface}} — {{steps}} +- **Observed:** {{observed}} +- **Why this priority:** likelihood {{level}} ({{why}}); impact {{level}} ({{why}}) +- **Smallest fix:** {{fix.strategy}} +- **Regression test:** {{fix.regression_test}} + + + +## Needs validation + +These are source-grounded leads blocked on one fact each. They are not +confirmed vulnerabilities and carry no severity. + +| Lead | Trace | Blocker | Local next step | Owner check | +|---|---|---|---|---| +| {{title}} | `{{entry file}}` → `{{sink file}}` | {{blocker}} | {{resolution.local}} | {{resolution.owner_check}} | + +## Hardening notes + + + +- {{note}} + +## Good patterns found + +- {{control that held, with its location}} + +## Coverage + + + +{{unit status table}} + +{{coverage gap table: every deferred, blocked, and out_of_scope unit with its reason}} + +**Final critic:** {{clean / not run — reason / returned N items, deferred}} + +**Not covered by this run:** {{companions outside the tier, paths outside the +scope, and anything deferred. State these as open, not as safe.}} + +**Suggested next run:** {{tier, profile, and the gaps it should start from}} diff --git a/engineering/security-audit/assets/sample_coverage_ledger.json b/engineering/security-audit/assets/sample_coverage_ledger.json new file mode 100644 index 00000000..248eea82 --- /dev/null +++ b/engineering/security-audit/assets/sample_coverage_ledger.json @@ -0,0 +1,414 @@ +{ + "schema_version": 1, + "run_id": "orders-api-run-1", + "units": [ + { + "coverage_id": "cu-0fd9027294b4ff3c369d", + "refs": { + "surface": "repository", + "boundary": "repository#baseline", + "subsystem": "repository", + "attack_class": "attack-classes.md#Baseline exposures" + }, + "label": "Whole repository: baseline exposures", + "starting_paths": [ + "config/settings.py", + "src/app.py" + ], + "class_blocks": [ + "attack-classes.md#Baseline exposures" + ], + "excluded_blocks": [], + "origin": "none", + "wave": 1, + "status": "covered", + "owner": "hunter-04", + "reviewed_paths": [ + ".gitignore", + "config/settings.py", + "requirements.lock", + "src/app.py" + ], + "checks": [ + { + "agent_id": "hunter-04", + "reviewed_paths": [ + "config/settings.py", + ".gitignore", + "requirements.lock", + "src/app.py" + ], + "invariant": "No shipped secret, open debug switch, or unauthenticated diagnostic route", + "method": "source", + "result": "every item checked; debug is gated on an environment value read once at start; no secrets or unprotected routes found", + "evidence_file": null + } + ], + "fingerprints": [], + "unresolved": [], + "attempts": [], + "hardening": [ + "Pin the two dependencies in requirements.lock that use a version range" + ] + }, + { + "coverage_id": "cu-4b2c324566475f50b679", + "refs": { + "surface": "src/api/orders.py#GET /orders/search", + "boundary": "src/db/orders.py#search_orders", + "subsystem": "services/orders", + "attack_class": "attack-classes.md#Injection" + }, + "label": "Order search: query construction from the q and sort parameters", + "starting_paths": [ + "src/api/orders.py", + "src/db/orders.py" + ], + "class_blocks": [ + "attack-classes.md#Injection" + ], + "excluded_blocks": [], + "origin": "none", + "wave": 1, + "status": "candidate", + "owner": "hunter-03", + "reviewed_paths": [ + "src/api/orders.py", + "src/api/schemas.py", + "src/db/orders.py" + ], + "checks": [ + { + "agent_id": "hunter-03", + "reviewed_paths": [ + "src/api/orders.py", + "src/db/orders.py" + ], + "invariant": "Caller text never becomes SQL structure", + "method": "source", + "result": "q is bound as a parameter; sort is interpolated into ORDER BY and looked suspicious", + "evidence_file": null + }, + { + "agent_id": "verifier-03", + "reviewed_paths": [ + "src/db/orders.py", + "src/api/schemas.py" + ], + "invariant": "The sort value is constrained before interpolation", + "method": "source", + "result": "schemas.py restricts sort to a four-value enum validated before the handler runs; no caller text reaches the query", + "evidence_file": null + } + ], + "fingerprints": [ + "orders:search:sort-interpolation" + ], + "unresolved": [], + "attempts": [] + }, + { + "coverage_id": "cu-4cd95d7167c66d6d8a54", + "refs": { + "surface": "src/api/orders.py#POST /orders/{id}/refund", + "boundary": "src/services/refunds.py#apply_refund", + "subsystem": "services/orders", + "attack_class": "attack-classes.md#Business logic" + }, + "label": "Refund flow: amount limits and repeated refunds", + "starting_paths": [ + "src/services/refunds.py" + ], + "class_blocks": [ + "attack-classes.md#Business logic" + ], + "excluded_blocks": [], + "origin": "critic", + "wave": 2, + "status": "covered", + "owner": "hunter-06", + "reviewed_paths": [ + "src/db/orders.py", + "src/services/refunds.py", + "tests/services/test_refunds.py" + ], + "checks": [ + { + "agent_id": "hunter-06", + "reviewed_paths": [ + "src/services/refunds.py", + "src/db/orders.py", + "tests/services/test_refunds.py" + ], + "invariant": "Total refunded never exceeds the amount paid, including under concurrent requests", + "method": "source", + "result": "refund and balance update run in one transaction with a row lock; negative and over-limit amounts are rejected before the write", + "evidence_file": null + } + ], + "fingerprints": [], + "unresolved": [], + "attempts": [ + { + "wave": 1, + "status": "covered", + "owner": "hunter-05", + "reviewed_paths": [ + "src/services/refunds.py" + ], + "checks": [ + { + "agent_id": "hunter-05", + "reviewed_paths": [ + "src/services/refunds.py" + ], + "invariant": "Refund amount is validated", + "method": "source", + "result": "amount is checked against the order total", + "evidence_file": null + } + ], + "fingerprints": [], + "unresolved": [], + "reopen_reason": "closed without reading the concurrent path in src/db/orders.py that the balance update uses" + } + ] + }, + { + "coverage_id": "cu-a8cbdb7fe84db79cd728", + "refs": { + "surface": "src/api/reports.py#GET /reports/export", + "boundary": "src/authz/policy.py#require_role", + "subsystem": "services/reporting", + "attack_class": "attack-classes.md#Feature abuse and data exposure" + }, + "label": "Report export: row scope of exported data", + "starting_paths": [ + "src/api/reports.py" + ], + "class_blocks": [ + "attack-classes.md#Feature abuse and data exposure" + ], + "excluded_blocks": [], + "origin": "none", + "wave": 1, + "status": "deferred", + "owner": null, + "reviewed_paths": [], + "checks": [], + "fingerprints": [], + "unresolved": [ + "budget_cannot_fund_hunters" + ], + "attempts": [] + }, + { + "coverage_id": "cu-b0309fd3c832ac96155e", + "refs": { + "surface": "src/api/auth.py#POST /password/reset", + "boundary": "src/mail/links.py#build_reset_url", + "subsystem": "services/identity", + "attack_class": "web-protocol-and-auth.md#Host and forwarded-header trust" + }, + "label": "Password reset: origin of the host used in emailed links", + "starting_paths": [ + "src/api/auth.py", + "src/mail/links.py" + ], + "class_blocks": [ + "web-protocol-and-auth.md#Host and forwarded-header trust", + "web-protocol-and-auth.md#Password reset and account recovery" + ], + "excluded_blocks": [], + "origin": "none", + "wave": 1, + "status": "candidate", + "owner": "hunter-02", + "reviewed_paths": [ + "config/settings.py", + "deploy/ingress.yaml", + "src/api/auth.py", + "src/mail/links.py" + ], + "checks": [ + { + "agent_id": "hunter-02", + "reviewed_paths": [ + "src/api/auth.py", + "src/mail/links.py", + "config/settings.py" + ], + "invariant": "Reset links are built from a configured origin, never from request headers", + "method": "source", + "result": "build_reset_url uses request.host when PUBLIC_BASE_URL is unset; settings.py leaves it unset by default", + "evidence_file": null + }, + { + "agent_id": "verifier-02", + "reviewed_paths": [ + "src/mail/links.py", + "deploy/ingress.yaml" + ], + "invariant": "The ingress in the repository fixes the Host header before it reaches the app", + "method": "source", + "result": "deploy/ingress.yaml routes by path only and does not set or restrict Host; production overlay is not in the repository", + "evidence_file": null + } + ], + "fingerprints": [ + "identity:reset:host-derived-link" + ], + "unresolved": [ + "whether the production proxy overrides or validates Host" + ], + "attempts": [] + }, + { + "coverage_id": "cu-b21b2524c4ae7d41843c", + "refs": { + "surface": "src/api/webhooks.py#POST /webhooks/test", + "boundary": "src/net/fetch.py#fetch_url", + "subsystem": "services/integrations", + "attack_class": "attack-classes.md#Files, fetches, and resource handling" + }, + "label": "Webhook test: server-side fetch of a caller-supplied URL", + "starting_paths": [ + "src/api/webhooks.py", + "src/net/fetch.py" + ], + "class_blocks": [ + "attack-classes.md#Files, fetches, and resource handling" + ], + "excluded_blocks": [], + "origin": "none", + "wave": 1, + "status": "blocked", + "owner": "hunter-07", + "reviewed_paths": [ + "src/api/webhooks.py", + "src/net/fetch.py" + ], + "checks": [ + { + "agent_id": "hunter-07", + "reviewed_paths": [ + "src/api/webhooks.py", + "src/net/fetch.py" + ], + "invariant": "The server refuses to fetch internal addresses, including after a redirect", + "method": "source", + "result": "an allowlist check runs on the first URL; redirect handling is delegated to a client wrapper whose configuration is loaded at run time", + "evidence_file": null + } + ], + "fingerprints": [], + "unresolved": [ + "redirect policy is set by a runtime config file not present in the repository and no sandbox was available to observe it" + ], + "attempts": [] + }, + { + "coverage_id": "cu-b85a24022c50ffa86b8e", + "refs": { + "surface": "web/src/App.tsx#router", + "boundary": "web/src/render/markdown.tsx#renderNote", + "subsystem": "web", + "attack_class": "attack-classes.md#Injection" + }, + "label": "Web client: rendering of user-written order notes", + "starting_paths": [ + "web/src/render/markdown.tsx" + ], + "class_blocks": [ + "attack-classes.md#Injection" + ], + "excluded_blocks": [], + "origin": "none", + "wave": 1, + "status": "out_of_scope", + "owner": null, + "reviewed_paths": [], + "checks": [], + "fingerprints": [], + "unresolved": [ + "companion_outside_tier:client-side.md" + ], + "attempts": [] + }, + { + "coverage_id": "cu-c565fdde20a55cc15828", + "refs": { + "surface": "src/api/orders.py#POST /orders/{id}/refund", + "boundary": "src/authz/policy.py#require_owner", + "subsystem": "services/orders", + "attack_class": "attack-classes.md#Access control" + }, + "label": "Refund endpoint: owner check on order refunds", + "starting_paths": [ + "src/api/orders.py", + "src/authz/policy.py" + ], + "class_blocks": [ + "attack-classes.md#Access control" + ], + "excluded_blocks": [ + { + "block": "attack-classes.md#Injection", + "reason": "handler takes a numeric path ID and a fixed enum reason; no interpreted sink" + } + ], + "origin": "none", + "wave": 1, + "status": "candidate", + "owner": "hunter-01", + "reviewed_paths": [ + "src/api/orders.py", + "src/authz/policy.py", + "src/db/orders.py", + "src/services/refunds.py", + "tests/api/test_refunds.py" + ], + "checks": [ + { + "agent_id": "hunter-01", + "reviewed_paths": [ + "src/api/orders.py", + "src/services/refunds.py", + "src/db/orders.py" + ], + "invariant": "A refund may only be issued by the tenant that owns the order", + "method": "source", + "result": "refund handler loads the order by ID alone; require_owner is applied to GET and PATCH but not to the refund route", + "evidence_file": null + }, + { + "agent_id": "hunter-01", + "reviewed_paths": [ + "tests/api/test_refunds.py" + ], + "invariant": "Tenant B cannot refund tenant A's order", + "method": "local", + "result": "existing test client with two fixture tenants: tenant B's request returned 200 and the fixture order moved to refunded", + "evidence_file": "agents/hunter-01/evidence/refund_cross_tenant.txt" + }, + { + "agent_id": "verifier-01", + "reviewed_paths": [ + "src/api/orders.py", + "src/authz/policy.py", + "src/services/refunds.py" + ], + "invariant": "No middleware or service-layer check enforces ownership on the refund path", + "method": "source", + "result": "re-read the route table and service; no ownership check exists between the handler and the write", + "evidence_file": null + } + ], + "fingerprints": [ + "orders:refund:missing-owner-check" + ], + "unresolved": [], + "attempts": [] + } + ] +} diff --git a/engineering/security-audit/assets/sample_findings.json b/engineering/security-audit/assets/sample_findings.json new file mode 100644 index 00000000..11c1906f --- /dev/null +++ b/engineering/security-audit/assets/sample_findings.json @@ -0,0 +1,175 @@ +{ + "schema_version": 1, + "run_id": "orders-api-run-1", + "findings": [ + { + "verdict": "needs_validation", + "fingerprint": "identity:reset:host-derived-link", + "title": "Password reset link may be built from a caller-supplied Host header", + "summary": "When PUBLIC_BASE_URL is unset, the reset email uses the request's Host header as the link origin. Whether an attacker can control that header depends on the production proxy.", + "suspected_root_cause": "build_reset_url falls back to request.host instead of failing when no public origin is configured", + "trace": [ + { + "step": "entry", + "file": "src/api/auth.py", + "line": 61, + "symbol": "request_password_reset", + "note": "unauthenticated request names the account to reset" + }, + { + "step": "sink", + "file": "src/mail/links.py", + "line": 19, + "symbol": "build_reset_url", + "note": "link origin taken from request.host when the setting is empty" + } + ], + "evidence": [ + { + "file": "config/settings.py", + "line": 34, + "note": "PUBLIC_BASE_URL defaults to an empty string" + }, + { + "file": "deploy/ingress.yaml", + "line": 12, + "note": "ingress in the repository does not set or restrict Host" + } + ], + "blockers": [ + "The production proxy configuration is not in the repository, so it is unknown whether an arbitrary Host header reaches the application" + ], + "resolution": { + "local": "Start the app from tests/conftest.py with PUBLIC_BASE_URL unset, post a reset request with a dummy Host, and read the link in the captured outbound mail fixture", + "owner_check": "Confirm whether the production load balancer forwards the client's Host header unchanged, and whether PUBLIC_BASE_URL is set in the production environment" + } + }, + { + "verdict": "confirmed", + "fingerprint": "orders:refund:missing-owner-check", + "title": "Any tenant can refund another tenant's order", + "summary": "The refund route loads the order by ID and issues the refund without checking that the caller's tenant owns it.", + "root_cause": "POST /orders/{id}/refund is registered without the require_owner dependency that the read and update routes use", + "violated_invariant": "Only the tenant that owns an order may change its payment state", + "actor": "Any authenticated user of any tenant", + "affected": "Orders and payment balances of every other tenant", + "trace": [ + { + "step": "entry", + "file": "src/api/orders.py", + "line": 88, + "symbol": "refund_order", + "note": "authenticated route; order ID taken from the path" + }, + { + "step": "flow", + "file": "src/services/refunds.py", + "line": 41, + "symbol": "apply_refund", + "note": "order fetched by ID with no tenant filter" + }, + { + "step": "sink", + "file": "src/db/orders.py", + "line": 120, + "symbol": "mark_refunded", + "note": "status and balance updated for the fetched order" + } + ], + "evidence": [ + { + "file": "src/api/orders.py", + "line": 52, + "note": "GET and PATCH routes declare Depends(require_owner); the refund route at line 88 does not" + }, + { + "file": "src/db/orders.py", + "line": 97, + "note": "get_order selects by id only" + } + ], + "preconditions": [ + { + "kind": "authentication", + "detail": "A valid session in any tenant" + }, + { + "kind": "data_state", + "detail": "The target order ID is known or guessed; IDs are sequential integers" + } + ], + "reproduction": { + "interface": "HTTP API through the repository's test client", + "inputs": [ + "POST /orders/1001/refund {\"reason\": \"duplicate\"} sent with tenant B's session; order 1001 belongs to tenant A" + ], + "steps": [ + "Load the two-tenant fixture from tests/api/conftest.py", + "Send the request as the tenant B fixture user", + "Read order 1001 back as tenant A" + ], + "observed": "Response 200; order 1001 status changed to refunded and tenant A's balance was reduced by the order total" + }, + "fix": { + "strategy": "Enforce ownership at the route with the same require_owner dependency the sibling routes use, and add the tenant to the service-layer lookup so the write path cannot be reached with a foreign order", + "regression_test": "tests/api/test_refunds.py: tenant B refunding tenant A's order returns 404 and leaves the order unchanged", + "changes": [ + { + "file": "src/api/orders.py", + "change": "Add Depends(require_owner) to refund_order" + }, + { + "file": "src/services/refunds.py", + "change": "Fetch the order with get_order_for_tenant(order_id, tenant_id)" + } + ] + }, + "severity": { + "likelihood": { + "level": "high", + "why": "Requires only an ordinary account and a sequential ID" + }, + "impact": { + "level": "high", + "why": "Cross-tenant write to payment state was demonstrated" + }, + "overall": "high" + }, + "confidence": { + "level": "high", + "why": "Reproduced locally by the hunter and independently re-read by a verifier" + } + }, + { + "verdict": "rejected", + "fingerprint": "orders:search:sort-interpolation", + "title": "Sort parameter interpolated into ORDER BY", + "summary": "The sort value is formatted into the query string, which looked like an injection point.", + "suspected_root_cause": "search_orders builds ORDER BY with string formatting", + "trace": [ + { + "step": "entry", + "file": "src/api/orders.py", + "line": 131, + "symbol": "search_orders_route", + "note": "sort taken from the query string" + }, + { + "step": "sink", + "file": "src/db/orders.py", + "line": 64, + "symbol": "search_orders", + "note": "sort formatted into ORDER BY" + } + ], + "evidence": [ + { + "file": "src/api/schemas.py", + "line": 27, + "note": "sort is an enum of four column names validated before the handler runs" + } + ], + "rejection_reason": "The request schema restricts sort to four fixed values, so no caller-controlled text reaches the query" + } + ] +} diff --git a/engineering/security-audit/assets/sample_run_metadata.json b/engineering/security-audit/assets/sample_run_metadata.json new file mode 100644 index 00000000..359104d6 --- /dev/null +++ b/engineering/security-audit/assets/sample_run_metadata.json @@ -0,0 +1,27 @@ +{ + "schema_version": 1, + "run_id": "orders-api-run-1", + "repo": "orders-api", + "target": "/home/auditor/code/orders-api", + "source_ref": { + "commit": "4b1e9c0d2a7f5e6b8c3d1f0a9e8b7c6d5a4f3e2b", + "dirty": false + }, + "tier": "focused", + "companions": [ + "web-protocol-and-auth.md", + "ai-and-llm.md", + "supply-chain-and-release.md", + "cloud-and-deployment.md" + ], + "profile": "standard", + "scope_paths": [], + "budget": 24, + "agents_spent": 24, + "execution_policy": "source-review-and-sandboxed-local-only", + "prior_runs": [], + "shared_file_owner": "lead", + "run_status": "complete", + "incomplete_reason": null, + "created": "2026-10-07T09:00:00Z" +} diff --git a/engineering/security-audit/references/ai-and-llm.md b/engineering/security-audit/references/ai-and-llm.md new file mode 100644 index 00000000..e0f3f1b4 --- /dev/null +++ b/engineering/security-audit/references/ai-and-llm.md @@ -0,0 +1,161 @@ +# AI, LLM, and Agent Companion + +Tier: focused, full. + +## Select this companion when + +A language model takes part in a decision that matters: an assistant or +chatbot, retrieval-augmented generation, persistent agent memory, a +tool-calling loop, an MCP server or client, code that builds prompts from +text someone else wrote, or code that acts on what a model returns. + +The flow to follow is: content an attacker can write, into a model's context +or memory, out to a capability, an authority, or a sink. Ordinary boundaries +around that flow — transport, per-object access, query building, rendering — +stay with the core classes. On a large target, give each stage of that flow +its own units. + +## Ground rules + +```text +- "The model can be talked into it" is not a finding. Point to the code that + let untrusted content reach another principal's context, use authority the + requester lacks, disclose data they cannot read, or drive a sink they cannot + reach themselves. +- Treat model output, memory entries, tool descriptions, and tool results as + untrusted input. The defect is in the code that trusts them. +- Instructions in a system prompt are not a control. Only deterministic checks + count: authorization on the named resource, isolation of context, binding of + an approval to an action, credentials with limited reach. +- Name the attacker, the affected principal, the identity the action runs as, + the resource, the exact action, and the visible result. +- A user asking the agent to do what that user may already do is intended + behaviour, however the request is phrased. +- Authorization and intent are different controls. An action that is + permitted for the victim but that the victim never asked for is a binding + failure when attacker-written content caused it. +- Use local fixtures and a stubbed or recorded model response. Do not call a + paid or hosted model API to demonstrate a defect. +``` + +## Hunting classes + +### Indirect injection through retrieved content + +Someone can write a document, page, email, ticket, file, or tool result that +later enters a different principal's context. For each source, establish its +authors, the filter applied when it is fetched, the session that ends up +reading it, and the powers that session holds. The defect is the missing isolation or the missing binding between +the content and the reader's intent. + +### Context shared across sessions or tenants + +Conversation state, vector stores, fetched passages, or cached prompts keyed +more broadly than the principal. Check that tenant and access filters are part of the +query and of every cache key, on every path including batch and background +jobs. A tenant field stored on the object but absent from the query is not +enforcement. + +### Memory poisoning + +Attacker-influenced content or a model's own summary is written to long-lived +memory and later shapes another task or another user's session. Review who can +create, update, merge, and delete memory, how its origin is recorded, and +whether a low-trust observation can be stored as a standing instruction. + +### Role and provenance spoofing in prompt assembly + +Untrusted text can pass as a system message, an earlier turn, a tool result, +or a policy because the prompt is built by concatenation, history is untyped, +the caller supplies a role field, or a serialization round trip drops the +source label. Confirm the forged origin changes a decision made in code or +unlocks a capability. + +### Tool arguments reaching a sink + +Model-produced arguments flow into a query, a shell, a file path, a URL fetch, +or a privileged API with no validation in the handler. Treat the tool schema +as a parser: it constrains shape, not safety. Trace each argument from the +decoded call to the sink exactly as for any other untrusted input. + +### Agent acting beyond the requester's authority + +The agent runs with a service identity or broad credential, and the tool +handler does not check whether the requesting user may perform that operation +on that resource. Compare with what the same user can do through the normal +product interface. + +### Approval not bound to the action + +A confirmation step exists, but what the human approved is not what executes: +arguments change after approval, approval is reusable, a summary differs from +the real call, or one approval covers a batch the user did not see. + +### Tool schema and dispatcher mismatch + +The dispatcher resolves tool names loosely, accepts fields the schema does +not declare, coerces types, or routes to a handler other than the one the +policy checked. Compare the declared schema, the validation code, and the +handler signature. + +### Runaway action loops + +A model-driven loop with no bound on steps, spend, recursion, or fan-out, where +attacker content can keep it going at the operator's cost or against a shared +resource. + +### Inherited trust in sub-agents and connected servers + +A sub-agent or connected server receives the parent's full context, +credentials, or tool set when the task needs a fraction, or its output is +merged back as trusted. Trace what crosses the boundary in each direction. + +### MCP server and tool identity + +Tools from different servers share a name space; a server can shadow or +redefine a tool, change its definition after approval, or be reached at an +address the user did not vet. Check how identity is pinned and re-verified. + +### Tool metadata treated as policy + +Descriptions, annotations, or schema text supplied by a tool provider decide +what is safe, what needs approval, or what the agent may read. Metadata from a +party you do not control is input. + +### Unsafe rendering of model output + +Model text rendered as HTML or rich markup, turned into links or images that +load automatically, or executed by a client, giving attacker content a path +to script execution or to leaking context through a requested URL. + +### Extraction of hidden context + +System prompts, other users' data, retrieved documents, or credentials placed +in context can be read back by a user who should not see them. This matters +when the context holds something that user cannot otherwise obtain; a system +prompt alone is rarely a secret worth a finding. + +## Cross-cutting moves + +- Draw the path as writer, store, retriever, context, model, dispatcher, + handler, sink. Ask who controls each arrow. +- For every tool, answer: whose identity runs it, who is checked, and on + which resource. +- Look for the second consumer: the same content read by a more privileged + agent, a scheduled job, or an admin view. +- Check what persists after the conversation: memory, caches, logs, + generated files. +- Replace the model with a stub returning attacker-chosen output. If the + system is unsafe under that stub, the defect is in the code. + +## Evidence bar + +- Confirmed needs a deterministic local reproduction: a fixture document or a + stubbed model response, the resulting tool call or output, and the affected + dummy principal. +- A defect that depends on how a particular hosted model responds is + `needs_validation`; state the response shape that would trigger it. +- Prompt wording, missing guard prompts, and jailbreak susceptibility are not + findings without a code-level boundary failure. +- Severity follows the action reached and the data exposed, not the fact that + injection was possible. diff --git a/engineering/security-audit/references/attack-classes.md b/engineering/security-audit/references/attack-classes.md new file mode 100644 index 00000000..8401e4c5 --- /dev/null +++ b/engineering/security-audit/references/attack-classes.md @@ -0,0 +1,243 @@ +# Core Attack Classes + +The nine classes every audit considers, whatever the breadth tier. Each `##` +heading below is a **block**: the ledger references it as +`attack-classes.md#` and the lead pastes the whole block into the +hunter's prompt. Blocks are self-contained on purpose — a hunter sees only what +it is given. + +Pick the classes reconnaissance supports. A CLI with no network listener does +not need a session-handling hunter; a multi-tenant API needs access control +split per subsystem. Add target-specific classes when the product has a trust +decision none of these names. + +Two rules apply to every block: + +- A class is a place to look, not a finding. A candidate still needs a + lower-trust actor, a control that should have stopped them, a traced path + past it, and a concrete result. +- Evidence stops at source review and bounded local checks. No block licenses + traffic to a deployed system. + +## Companion routing + +When reconnaissance finds one of these boundaries and the run's tier includes +the companion, select blocks from it alongside the core class. + +| Boundary found in source | Companion | Tier | +|---|---|---| +| Sessions, CSRF, JWT, OAuth/OIDC, SAML, MFA, API keys, mTLS, proxies, caches | `web-protocol-and-auth.md` | focused, full | +| Prompts built from untrusted text, RAG, agent memory, tool calls, MCP | `ai-and-llm.md` | focused, full | +| Dependency resolution, CI workflows, release, signing, updaters, plugins | `supply-chain-and-release.md` | focused, full | +| IAM, infrastructure as code, containers, serverless, ingress, secrets config | `cloud-and-deployment.md` | focused, full | +| SPA rendering, extensions, service workers, cross-window messaging, CORS | `client-side.md` | full | +| Multi-tenant stores, caches, search, export, backup, deletion, restore | `data-isolation-and-lifecycle.md` | full | +| Native apps, deep links, webview bridges, privileged helpers, local sockets | `desktop-mobile-and-local-ipc.md` | full | +| C/C++/unsafe Rust, parsers, FFI, loaders, JITs, kernel interfaces | `memory-safety-and-binary.md` | full | +| gRPC, custom wire formats, queues, brokers, webhooks, streaming | `protocols-rpc-and-messaging.md` | full | +| Untrusted work consuming shared CPU, memory, workers, quota, or spend | `resource-exhaustion-and-availability.md` | full | + +A boundary whose companion is outside the tier is not skipped silently: seed a +unit for it under the closest core class and mark it `out_of_scope` with the +reason `companion_outside_tier:`, so the report names the gap and a later +run can pick it up. + +## Injection + +Follow untrusted input from where it enters to where it is interpreted. The +sink decides the bug, so list the sinks first: + +- **Query and command builders** — SQL, NoSQL operators, shell command lines, + LDAP and XPath filters, ORM raw fragments, search DSLs. +- **Renderers and templates** — HTML, server-side templates, email bodies, + PDF and document generators, log lines read by another system. +- **Path and URL builders** — filesystem paths, redirect targets, request + URLs, header values, cache keys. +- **Decoders** — deserializers, expression evaluators, regex compiled from + input, format strings, dynamic imports. + +Then look past the direct path: + +- Stored-then-used: a value written safely and read later by code that trusts + the store. +- Names as well as values: object keys, column names, header names, filenames, + sort and filter parameters. +- Secondary systems: logs, metrics labels, search indexes, analytics exports, + message payloads consumed elsewhere. +- Encoding seams: a check on the decoded form and a use of the raw form, or + the reverse. + +The check that matters is at the sink: parameter binding, context-correct +encoding, an allowlist of structural tokens. A filter upstream of a second +decode is not a control. + +## Access control + +Establish, for each state change and each read of protected data, which +principal is allowed and where the code decides. Presence of a check is not +the question; whether it is the right check, on the right object, on every +path, is. + +- Does every route to the same effect apply the same decision — the main + handler, the bulk variant, the import, the internal API, the legacy version? +- Is the object loaded by an identifier the caller supplies, and is ownership + or tenancy checked on the loaded object rather than on the request? +- Can a request field set something the permission model meant to fix: owner, + tenant, role, price, status, a foreign key? +- Are there handlers that require a login and nothing else? +- Do list, search, count, and export operations filter per item, or only gate + the operation? +- Are role and membership changes re-read at decision time, or cached in a + token or session that outlives the change? + +When the model is complex, split the work: one unit for who can authenticate +as what, one for per-object decisions. + +## Files, fetches, and resource handling + +Anywhere the target opens, writes, fetches, unpacks, or parses something the +caller names: + +- **Paths** — traversal through `..`, absolute paths, encoded separators, + symlinks inside an upload or archive, case and Unicode normalization + differences between the check and the filesystem. +- **Server-side fetches** — a URL, host, or webhook target the caller + controls; checks that validate the first URL but not the redirect; resolving + the name twice; differing URL parsers between the validator and the client. +- **Archives and uploads** — entries that escape the extraction root, type + decided by extension or client header, files served back from an executable + or same-origin location. +- **Decoding** — object deserialization of untrusted bytes, XML with external + entities, parsers with unbounded nesting. +- **Check-then-use gaps** — a file validated and then reopened by name, a temp + file created in a shared directory with a predictable name. +- **Native memory** — if the target has unsafe code and the tier excludes the + memory-safety companion, record the surface and mark it out of scope. + +## Cryptography and secrets + +Look at how the code uses primitives, not which library it imports. + +- Security values from a non-cryptographic generator: tokens, reset codes, + session IDs, nonces, filenames that act as capabilities. +- Secrets in source, fixtures that ship, logs, error bodies, URLs, client + bundles, or analytics events. +- Verification that can be skipped: a signature checked only when present, an + algorithm chosen by the message, a comparison that returns early. +- Encryption without authentication, reused nonces or IVs, keys derived from + low-entropy input without a slow KDF. +- Failure behaviour: when decryption, verification, or key loading fails, + does the code stop, or continue with a default? +- Key scope: one key for several purposes or tenants, no rotation path, old + keys accepted indefinitely. + +A weak primitive is a finding only when the value it protects matters and an +actor can reach it. Otherwise it is a hardening note. + +## Business logic + +Scanners do not find these; reading the workflow does. For each multi-step +process that moves money, grants access, or changes ownership: + +- **Order of steps** — skip one, repeat one, run them out of order, resume an + abandoned flow, replay a completed one. +- **Partial failure** — step two fails after step one committed. What state + remains, and who benefits from it? +- **Concurrency** — two requests that each pass the check before either + writes: balance, quota, coupon, inventory, approval, uniqueness. +- **Numbers** — negative, zero, fractional, very large, wrong unit, wrong + currency, rounding in the caller's favour, string where a number is expected. +- **Equivalent operations** — a limit enforced on one operation and absent on + another that reaches the same outcome. +- **Trusted stored data** — values assumed valid because the write path + validated them, when a second write path does not. +- **Time** — expiry at the exact boundary, clock differences between + services, time zones, windows that reset. +- **Defaults** — behaviour when configuration is missing, a flag is off, a + dependency is down, a migration is half applied. + +State the business rule in one sentence before claiming it is broken. + +## Feature abuse and data exposure + +Working features used for something they were not meant to do. + +- **Export, report, backup** — includes rows above the requester's access, + other users' records, deleted or draft items, fields the UI hides. +- **Import, restore, sync** — overwrites existing records, skips validation + the normal write path applies, writes into collections the user cannot + otherwise modify. +- **Search, filter, sort, autocomplete** — reveals existence or values of + records the caller cannot read; ordering by a hidden field leaks it. +- **Different answers for different reasons** — distinct errors, status + codes, sizes, or timing for "does not exist" versus "not yours", in login, + reset, invite, and lookup flows. +- **Previews, drafts, share links** — a token meant for one item that opens + more; unpublished content reachable through feeds, sitemaps, or listings. +- **User-supplied callbacks** — webhook, notification, avatar, or import URLs + the server fetches. + +## Chained trust gaps + +Steps that are each acceptable alone and unsafe together. Build the chain only +from links the source establishes. + +- **Guarantee versus assumption** — write down what component A actually + guarantees about a value (type, length, normalization, tenant) and what + component B assumes. The gap is the finding. +- **Second-order use** — a slug becomes a path, a display name becomes a + header, a stored string becomes a template, a field name becomes a query key. +- **Capability growth** — a token, API key, plugin, or delegated grant that + gains reach after refresh, caching, role change, or composition with another + feature. +- **Ordering windows** — between validate and consume, revoke and cache + expiry, soft delete and purge, setup and first use. +- **Undo paths** — restore, undelete, rollback, and cancel must apply today's + ownership and validation, not the rules in force when the record was made. + +Every link is a prerequisite. If one is not established, the chain is a +`needs_validation` lead with that link as the blocker, or nothing. + +## Wildcard + +No category is assigned. Look for what the other units will not. + +- Code that is odd, old, or apologetic: `legacy`, `compat`, `temp`, `hack`, + comments explaining why something is safe. +- Half-finished or experimental features, and flags that expose them. +- What the API accepts that the product's own client never sends. +- Routes, parameters, and headers registered in code but absent from docs. +- Features combined that were built separately: preview with caching, import + with plugins, impersonation with API keys. +- Assumptions about the environment: case-sensitive filesystem, accurate + clock, trusted DNS, local-only database. +- What the tests cover, and the edge cases nobody wrote a test for. +- Version history, where it is available locally: reverted fixes, removed + checks, secrets committed and later deleted. + +Follow anything strange until you can say in one sentence why it is safe or +what boundary it breaks. The evidence bar does not drop because the unit is +open-ended. + +## Baseline exposures + +Literal, exhaustive, unglamorous. Everyone assumes someone else checked these. + +- Credentials, keys, tokens, or private key material in the repository, + including fixtures, examples, and seed data that could work in production. +- Comments admitting a missing control (`TODO auth`, `FIXME validate`). +- Debug or development modes that a request parameter, header, or environment + variable can switch on. +- Diagnostic, admin, metrics, health, or config endpoints with no access + check. +- Ignore rules that fail to cover secrets, uploads, and local config. +- Unpinned dependencies and lockfile entries with known advisories. +- Dynamic code evaluation fed by input. +- Cross-origin policy that reflects any origin with credentials. +- Session cookies missing transport and script-access restrictions. +- Redirect parameters passed through unchecked. +- Error responses that return stack traces, queries, or filesystem paths. + +Check every item and record the result for each, including "not present". A +match is a starting point, not a finding: confirm the flagged value is real, +reachable, and protects something before reporting it. diff --git a/engineering/security-audit/references/client-side.md b/engineering/security-audit/references/client-side.md new file mode 100644 index 00000000..00138572 --- /dev/null +++ b/engineering/security-audit/references/client-side.md @@ -0,0 +1,143 @@ +# Client-Side and Browser Companion + +Tier: full. + +## Select this companion when + +The repository ships code that runs in a browser or a browser-like host and +makes trust decisions there: single-page applications, browser extensions, +embedded web views, service workers, cross-window messaging, client storage of +tokens or user data, cross-origin configuration, or WebSocket clients. + +Server-rendered injection belongs to the core `Injection` class. This file +covers defects that exist because of how the client builds the page, talks +across origins, or stores state. + +## Ground rules + +```text +- Name the attacker's position: another website the victim visits, another + frame or window, a lower-privileged user whose content the victim views, or + a script already running in a less-trusted part of the page. +- Trace from a source the attacker controls (location, referrer, message + data, storage, fetched content, another user's input) to a sink that acts + (markup insertion, script evaluation, navigation, a privileged API call). +- Framework escaping is a real control. The finding is where the code steps + around it: raw HTML insertion, dynamic attribute or URL construction, + template compilation at run time. +- Browser behaviour that varies by version or vendor, and response headers + set by infrastructure outside the repository, are unknown unless the + repository pins them. +- Reproduce in a local browser or a DOM test harness with dummy accounts and + a local origin. No third-party sites, no deployed hosts. +``` + +## Hunting classes + +### DOM-based script injection + +Attacker-controlled data reaches an HTML, script, or URL sink in client code: +raw markup insertion, framework escape hatches, script or handler attributes, +`javascript:` URLs, dynamically chosen component or template names. Include +sanitizers configured too loosely or applied before a later mutation. + +### DOM clobbering + +Markup the attacker can supply defines named elements that shadow variables or +properties the script later reads as configuration, turning harmless-looking +HTML into control over a URL, a flag, or a script path. + +### Prototype pollution + +Recursive merge, deep clone, path-set, or query-string parsing that lets input +write to a shared prototype. A pollution primitive needs a gadget: find the +later code that reads the polluted property and what it does with it. + +### Cross-window message trust + +A message listener that does not check the sender's origin, checks it with a +loose match, or trusts the message's own claims; and a sender that posts +sensitive data to a wildcard target. Follow accepted message data to its sink. + +### Cross-site WebSocket use + +A socket endpoint that authenticates with ambient cookies and does not check +the origin of the handshake, letting another site open a connection as the +victim and read or send messages. + +### Credentialed cross-origin policy + +Cross-origin rules that reflect the caller's origin, accept a null origin, or +match by prefix or suffix, while allowing credentials. Establish which +authenticated responses become readable to another site. + +### Service worker scope and takeover + +A worker script that can be influenced by user content, registered from a +path an attacker can write to, or scoped more widely than the feature that +owns it, giving persistent control over requests for that origin. + +### Service worker cache and identity + +A worker serving cached responses without regard to who is signed in, or +after logout, so one user's data is shown to the next on a shared device, or +stale authorization persists offline. + +### Browser storage of sensitive state + +Tokens, keys, or personal data kept in script-readable storage where any +injection or extension reads them; data that survives logout; authorization +decisions made from client-held values the server does not re-check. + +### Shared storage and broadcast channels + +Several apps or trust levels on one origin sharing storage, channels, or +events, so a less-trusted area can read or forge messages meant for another. + +### Cross-site state leaks + +Another site can learn something about the victim's state without reading +the response: timing, frame counts, error versus load events, redirect +behaviour, cache probes, navigation length. Matters when the state is +sensitive — membership, search results, identity. + +### Opener and window reference leaks + +Links and pop-ups that leave the opened page with a reference to the opener, +or expose a window handle that lets a less-trusted page navigate or message a +trusted one. + +### Framing and UI redress + +Sensitive single-click actions on a page that can be framed by another site. +Establish which action, whether it needs only clicks, and whether the +repository sets framing policy or leaves it to infrastructure. + +### Client-side routing and redirect confusion + +Router or redirect logic that takes a destination from the URL, accepts +scheme-relative or encoded forms, or parses differently from the server, used +to send a signed-in user or a token to an attacker's page. + +## Cross-cutting moves + +- List sources and sinks for each route, then connect them. +- Search for every escape hatch the framework offers and justify each use. +- Check the signed-out, signed-in, and just-signed-out states of the same + page. +- Test encodings and the URL fragment as well as the query; client code often + reads both. +- Read third-party script inclusion: what loads from where, with what + integrity guarantee, and with what access to the page. + +## Evidence bar + +- Confirmed needs a local reproduction: the crafted URL, message, or stored + value, and the script-controlled result in a local page or DOM harness. +- Where exploitability depends on a response header or browser default not + fixed in the repository, record `needs_validation` with that fact as the + blocker. +- Injection that only affects the user who supplies it is not a finding + unless another user or a higher-privilege context renders it. +- Missing content security policy or framing headers are hardening notes + unless they are the only thing between a traced input and a sink. diff --git a/engineering/security-audit/references/cloud-and-deployment.md b/engineering/security-audit/references/cloud-and-deployment.md new file mode 100644 index 00000000..fa4dc839 --- /dev/null +++ b/engineering/security-audit/references/cloud-and-deployment.md @@ -0,0 +1,156 @@ +# Cloud and Deployment Companion + +Tier: focused, full. + +## Select this companion when + +The repository defines how the system is deployed and what it may reach: +infrastructure-as-code, IAM policies and role bindings, container and +orchestrator manifests, service-mesh or ingress configuration, serverless and +edge function definitions, event triggers, or the wiring of secrets and +runtime configuration. + +This is a source audit of deployment definitions and of application code that +relies on them. It does not query a cloud account. A posture review of live +resources is a different exercise. + +## Ground rules + +```text +- Work only from definitions in the repository and from rendering them + locally (template output, policy evaluation with dummy identities). Never + call a cloud API or read live state. +- The deployed account may differ from the repository. A control you cannot + see is unknown, not absent. If it decides the question, the lead is + needs_validation and the owner check names the exact resource and setting. +- A broad permission is a finding only with a path: which workload holds it, + how a lower-trust actor comes to act as that workload, and what they can + then reach. +- Distinguish the application trusting the platform from the platform being + misconfigured. Both matter; they have different fixes. +- Render overlays for every environment the repository defines. The + production overlay is the one that counts, and the difference between + overlays is often the finding. +``` + +## Hunting classes + +### Workload identity with excess reach + +A service, function, job, or pod whose role allows far more than its code +uses: wildcard actions or resources, the ability to pass or assume other +roles, write access to its own policy or deployment. Combine with any code +path that lets a caller steer the workload's requests. + +### Cross-account and cross-tenant role assumption + +Trust policies that let an external principal assume a role without a +condition binding it to the intended caller, missing external identifiers, +wildcard principals, and tenant-supplied role or resource names used without +validation. + +### Authorization delegated to resource metadata + +Application decisions based on tags, labels, names, or bucket paths that a +lower-trust actor can set. If writing the label is cheaper than gaining the +access it confers, the label is not a control. + +### Unintended service or management exposure + +Listeners, admin consoles, debug ports, orchestration APIs, dashboards, and +databases reachable from wider networks than intended: public load balancers, +permissive ingress rules, host networking, node ports, default-allow policies. + +### Trusted proxy and mesh identity bypass + +Application code that trusts an identity header or a network position because +a gateway or sidecar is expected to enforce it, while a route exists that +does not pass through that component: direct pod access, an internal service +port, a second ingress. + +### Metadata service and internal endpoint reach + +A workload that can be made to request instance metadata or internal control +endpoints on a caller's behalf, where the definitions do not restrict that +access. Pair with the core server-side fetch class. + +### Container privilege and host access + +Privileged mode, added capabilities, host namespaces, host path mounts, +writable runtime sockets, root users with a writable root filesystem, and +missing seccomp or equivalent profiles on workloads that process untrusted +input. + +### Admission and policy path gaps + +Policy enforced on one creation path and not another: a controller that +validates deployments but not jobs, namespaces exempt from policy, mutating +steps that run after validation, policy in audit-only mode. + +### Namespace and label trust + +Network policy, access rules, or scheduling that select on labels or +namespaces a tenant or low-privilege team can create or edit. + +### Configuration precedence drift + +Several sources set the same security-relevant value — defaults, files, +environment, flags, remote config — and the effective one is weaker than the +reviewed one. Follow precedence in code and in each overlay; look for a debug +or bypass flag reachable in production. + +### Secrets crossing workload boundaries + +Secrets mounted into workloads that do not need them, shared across +environments or tenants, written to logs or environment dumps, baked into +images, or readable through a broad secret-store policy. + +### Credential renewal failure fallback + +What the code does when a token cannot be refreshed, a certificate has +expired, or the secret store is unreachable: continue with a stale +credential, skip verification, or fall back to a static key. + +### Object storage and signed URL policy + +Buckets or containers with public or cross-account access, signed URLs with +long lifetimes or attacker-chosen keys and content types, upload policies that +allow overwriting other tenants' objects, and listings exposed alongside +objects. + +### Event source identity + +Functions and consumers triggered by queues, topics, storage events, or +webhooks that do not verify which source produced the event, so anyone able +to publish to the source — or to a look-alike — drives privileged code. + +### Edge and origin disagreement + +Rules at the edge (auth, path rewriting, header normalization, caching) that +the origin assumes were applied, with an origin still directly reachable or an +edge rule that normalizes differently from the application. + +## Cross-cutting moves + +- For each workload, write: identity, what that identity can do, who can + influence its inputs, what it can reach on the network. +- Diff environments. A control present in staging and absent in production, + or the reverse, deserves a unit. +- Look for the second door: a direct service address behind every gateway. +- Trace each secret from store to consumer to log. +- Check defaults: what applies to a new namespace, a new bucket, a new role + when nobody sets anything. + +## Evidence bar + +- Confirmed needs the definition in the repository, the rendered effective + configuration, and a local demonstration where one exists: a policy + evaluation with dummy principals, a rendered manifest, a unit test of the + application code that trusts the platform. +- Whether the live environment matches the repository is always the owner's + to confirm. If the impact depends on that match, write the owner check and + keep the lead as `needs_validation`. +- Permissive settings with no reachable actor or no meaningful resource are + hardening notes. +- Severity follows reachable authority: control-plane or cross-tenant reach is + high; a broad role on an isolated batch job is not, by itself. diff --git a/engineering/security-audit/references/data-isolation-and-lifecycle.md b/engineering/security-audit/references/data-isolation-and-lifecycle.md new file mode 100644 index 00000000..423015bb --- /dev/null +++ b/engineering/security-audit/references/data-isolation-and-lifecycle.md @@ -0,0 +1,146 @@ +# Data Isolation and Lifecycle Companion + +Tier: full. + +## Select this companion when + +The system keeps data for more than one tenant, team, or user in shared +infrastructure, or data moves through copies with their own lifetimes: caches, +search indexes, object storage, analytics pipelines, exports, backups, +replicas, migrations, soft deletion, retention, restore. + +The core `Access control` class asks whether the primary read and write paths +check the right thing. This file follows the data after that: every other +reader, every derived copy, and every point in the record's life where the +rule can lapse. + +## Ground rules + +```text +- State the isolation rule in one sentence: which principal may see which + record, keyed on what. Then find each place the code must apply it. +- Enforcement lives in the query, the key, or the storage policy. A tenant + column that the query does not filter on is documentation. +- Follow copies. For every record type, list where it is duplicated or + derived: cache, index, queue, log, analytics event, export file, backup, + replica. Each copy needs the rule or needs to be unreachable. +- Follow time. Check the rule at creation, update, share, unshare, role + change, deletion, retention expiry, and restore. +- Demonstrate with two dummy tenants in a local store. Never read real + customer data or a shared environment. +``` + +## Hunting classes + +### Missing tenant or owner scope + +A query, lookup, or update that selects by record identifier alone where the +rule requires tenant or owner as well. Check single reads, lists, counts, +joins, bulk operations, background jobs, and admin or support tools. + +### Composite key and namespace collision + +Keys built by joining parts without an unambiguous separator, truncation or +case-folding that makes two tenants' keys equal, slugs or usernames reusable +after deletion, and identifiers unique per tenant used in a global namespace. + +### Policy and query disagreement + +The authorization layer decides on one representation and the data layer +fetches by another: a policy evaluated on the parent while the query returns +children, a filter applied after pagination, row-level rules bypassed by a +raw query or a privileged connection. + +### Access by possession of a link or key + +Object keys, file URLs, or signed links that grant access by possession: +predictable or enumerable paths, links that outlive the permission, a +signature covering a prefix instead of an object, and a shared bucket with no +per-tenant policy. + +### Search, cache, and index drift + +A derived store returns records the primary store would refuse: indexes built +without access fields, cache keys missing the principal or tenant, results +filtered in the application after ranking, permissions changed in the source +and never propagated. + +### Logs, analytics, and traces as readers + +Sensitive fields copied into logs, events, traces, error reports, or support +dashboards that a wider audience can read, or that a tenant can query through +an analytics feature. + +### Enumeration and aggregate oracles + +Counts, totals, autocomplete, uniqueness errors, and sequential identifiers +that reveal the existence or size of another tenant's data without returning +the records. + +### Export and backup scope + +Export, report, and backup jobs that run with a privileged connection and +select more than the requester may see, or whose output lands somewhere with +weaker access than the source. + +### Import and restore authority + +An import, sync, or restore that writes records with identifiers, owners, or +tenants taken from the file, letting the caller place or overwrite data +outside their scope. + +### Migration defaults and ownership + +A schema or data migration that assigns a default owner, tenant, or +visibility to existing rows, backfills permissions too broadly, or leaves a +window in which the new column is unset and the check passes. + +### Replica and backup boundary drift + +Read replicas, warehouses, staging copies, and backups that hold production +data under different access rules, encryption, or retention than the primary. + +### Soft delete bypass + +Records marked deleted that remain reachable by direct identifier, through a +relationship, in search, in exports, or by an endpoint that forgets the +filter. + +### Stale authorization in derived copies + +Access removed at the source — unshare, role change, membership removal, +tenant offboarding — while tokens, cached decisions, materialized views, or +queued jobs continue to act on the old grant. + +### Retention and queued work overrun + +Data that outlives its promised lifetime in queues, dead-letter stores, +temporary files, or scheduled jobs, and work queued before a deletion that +recreates or re-exposes the record afterwards. + +### Restore of invalid state + +Undelete, version rollback, or backup restore that brings back a record with +its old owner, sharing, or validation state without applying current rules. + +## Cross-cutting moves + +- Build the copy map for each sensitive record type, and check each copy's + key and its reader. +- Run every list and search path as tenant B looking for tenant A's fixture. +- Change a permission, then exercise each derived store before any refresh. +- Delete a record, then look for it by identifier, by relation, in search, in + export, and after restore. +- Find the privileged connection and list every code path that uses it. + +## Evidence bar + +- Confirmed needs a local two-tenant reproduction: tenant B's request and + tenant A's fixture record, field, or existence returned or changed. +- Where isolation depends on storage policy, database roles, or + infrastructure not in the repository, record `needs_validation` and name + the policy the owner should check. +- Exposure of a principal's own data to themselves, or of non-sensitive + metadata, is not an isolation failure. +- Severity follows what crosses the boundary and how far: arbitrary + cross-tenant read or write is high; existence of a record is usually low. diff --git a/engineering/security-audit/references/desktop-mobile-and-local-ipc.md b/engineering/security-audit/references/desktop-mobile-and-local-ipc.md new file mode 100644 index 00000000..0529d1da --- /dev/null +++ b/engineering/security-audit/references/desktop-mobile-and-local-ipc.md @@ -0,0 +1,156 @@ +# Desktop, Mobile, and Local IPC Companion + +Tier: full. + +## Select this companion when + +The target runs on a user's device and has boundaries there: native desktop +or mobile apps, custom URL schemes and deep links, web views with a native +bridge, exported app components, privileged helpers or services, local +daemons, and local channels such as Unix sockets, named pipes, loopback +ports, or platform IPC. + +On a device the lower-trust actor is usually another app, a web page, a +document, or an unprivileged local user — not a remote network client. + +## Ground rules + +```text +- Name the local attacker precisely: a web page in the user's browser, + another installed app without special permission, a file the user opens, an + unprivileged account on a shared machine, or a second profile on the device. +- Name the boundary: app sandbox, user account, privilege level, web origin + versus native code, signed-in account. +- An attacker who already has the victim's own privilege inside the same + sandbox has crossed nothing. Say what they gain that they did not have. +- Platform defaults, entitlements granted at signing, and store review are + outside the repository unless the manifest or project file declares them. + Read those files; treat the rest as unknown. +- Reproduce in a local build, emulator, or test harness with dummy accounts, + inside the sandbox the lead confirmed. Never install onto or probe a device + you do not control. +``` + +## Hunting classes + +### Custom scheme and deep link handling + +Links any app or page can open that trigger an action or carry parameters +into navigation, a web view, a file path, or an API call. Check which hosts +and paths the handler accepts, how it parses them, and whether verified +link association is required or optional. + +### App-to-app and account handoff + +Sign-in callbacks, share targets, and "open in" flows where another app can +intercept the response, inject one of its own, or cause an action in an +account the user did not choose. + +### File open and intent authority + +Opening a file, a document type, or an incoming intent causes the app to act +with its own permissions on a path or resource the sender picked: reading +private files, overwriting data, importing configuration. + +### Webview navigation reaching the bridge + +A web view that exposes native functions and can be navigated to, or made to +load, content the app does not control: unchecked URLs, redirects, injected +frames, cleartext loads. Establish which origin can call which native method. + +### Over-broad native bridge + +Bridge methods that take paths, URLs, commands, or selectors from page script +and act on them with native privilege, without checking the caller or +constraining the argument. + +### Webview file access + +Web views allowed to read local files, cross from file to network origins, or +load content from shared storage, turning a script-injection or a crafted +file into a read of the app's private data. + +### Local IPC peer authentication + +A socket, pipe, loopback port, or platform service that accepts any local +caller. Check how the server identifies the peer — credentials from the +channel, code-signature checks, access control on the endpoint — and whether +a browser page can reach a loopback listener. + +### Claimed identity versus channel identity + +The service authenticates the connection one way and then trusts a user, +process, or bundle identifier supplied in the message. Identifiers that can +be reused or raced between check and use belong here. + +### Exported components + +Activities, services, receivers, providers, extension points, and scripting +interfaces reachable by other apps without a permission, or with one any app +can obtain. Follow what each does with caller-supplied data. + +### IPC request lifecycle and correlation + +Replies delivered to the wrong caller, callbacks retained after the caller +disconnects, requests accepted before initialization or after teardown, and +state shared between sessions of different clients. + +### Privileged helper as confused deputy + +A helper running with elevated rights performs file, process, network, or +configuration operations on request, using arguments the unprivileged caller +controls: paths, commands, package locations, environment. + +### Install, update, and repair paths + +Installers, updaters, and repair tools that run elevated and read from +user-writable locations, verify a package and then reopen it by path, or load +libraries from a directory the user controls. + +### Local file ownership and races + +Sensitive files created with permissive modes or in shared directories, +predictable temporary names, and check-then-open sequences an unprivileged +user can win with a link. + +### Credential store boundaries + +Secrets stored outside the platform's protected store, in a store entry +shared with other apps from the same vendor group, exported in backups, or +accessible while the device is locked when they should not be. + +### Account switch, logout, and restore residue + +Data, tokens, caches, notifications, or web-view state from one account +visible after switching or signing out, or restored onto a different device +or profile. + +### Pending actions and user presence + +A sensitive action queued or confirmed while the app was in one state and +completed in another: an approval surviving a lock, a prompt that another +window can cover or answer, a biometric check not tied to the action. + +## Cross-cutting moves + +- Enumerate every externally reachable entry from the manifest and project + files before reading handlers. +- For each IPC endpoint, answer: who can connect, how is the peer identified, + what is trusted from the message. +- Read privileged code as if every argument came from an attacker. +- Exercise lifecycle edges: first launch, backgrounded, locked, logged out, + account switched, upgraded, restored from backup. +- Compare platforms. A check present on one and missing on its sibling is a + unit. + +## Evidence bar + +- Confirmed needs a local reproduction: the crafted link, intent, message, or + file, sent by a dummy lower-trust sender, and the privileged effect + observed. +- Where the outcome depends on platform version, signing entitlements, or + device policy not declared in the repository, record `needs_validation`. +- Actions that require the victim's own unlocked session and privilege, with + no boundary crossed, are not findings. +- Severity follows the privilege gained: code execution as a higher-privilege + user or theft of another app's or account's secrets is high. diff --git a/engineering/security-audit/references/hunting.md b/engineering/security-audit/references/hunting.md new file mode 100644 index 00000000..b24c28ef --- /dev/null +++ b/engineering/security-audit/references/hunting.md @@ -0,0 +1,252 @@ +# Phase 2 — Coverage-Led Hunting + +Hunters are assigned from the ledger, not from intuition. Each one owns a small +set of related units, reads the code behind them at depth, and returns one +structured result. A critic then looks for what the wave missed, and the loop +repeats until the critic finds nothing or the run's limits stop it. + +## Assigning a wave + +1. Take `planned` units in priority order: + 1. surfaces reachable with no authentication, then lowest-trust + authenticated surfaces; + 2. boundaries guarding credentials, cross-tenant data, code execution, or + release authority; + 3. reopened gaps, revalidations, and changed source before re-checks of + unchanged covered units; + 4. ties broken by `coverage_id`, so two runs order work the same way. +2. Group units for one hunter only when they share a subsystem and a boundary. + Do not mix unrelated boundaries to save an agent. +3. For each assignment, set the units to `in_progress`, set `owner` to a new + agent ID, and create `agents//scratch/`. Validate the ledger. +4. A unit the budget cannot reach becomes `deferred` with the reason. It is + never left `planned` and never dropped. + +Hunters read source, write only inside their own `scratch/`, and return their +result as their final message. They do not edit the target, the shared run +files, or another agent's directory. + +## Building the hunter prompt + +Assemble these parts in this order. Paste content, not names — a hunter cannot +open the skill's files. + +1. Role, in two sentences: find source-grounded failures of a security + invariant in the assigned units; return exactly one JSON object in the + result format below. +2. `architecture.md`, whole. +3. The assigned units: `coverage_id`, `label`, `refs`, `starting_paths`. +4. Every block in each unit's `class_blocks`, copied in full. For each + companion involved, also its `Ground rules`, `Cross-cutting moves`, and + `Evidence bar`. +5. The unit's `excluded_blocks` with reasons. +6. The hunting method and the candidate gate below. +7. Root causes to skip: for each carried confirmed record, its fingerprint, + title, and root cause only. Plus the coverage IDs other hunters own. +8. The agent's ID, its scratch path, and the result format, including the + `confirmed` and `needs_validation` definitions from + `assets/findings.schema.json`. + +### Hunting method — paste into every hunter prompt + +```text +HOW TO WORK + +You are looking for places where the code fails to enforce a security rule it +is supposed to enforce, and for the smallest change that would fix each one. +You work from source and, where permitted, small local checks with dummy data. +You never contact a deployed system, a provider API, a registry, an identity +service, a shared queue, or another person's account. + +Start from a rule, not from a pattern. For each assigned unit: + 1. Who is the lower-trust actor, and what can they already do? + 2. What value, action, or state change do they supply? + 3. Which control is supposed to stop it, scope it, or take it back? + 4. What does the code actually do after that control? Trace it. + 5. What is the smallest wrong outcome: a record that should be unreadable, + a wrong return value, a state that should be impossible? + 6. What source change and what regression test would enforce the rule? + +Read deeply. Stay with the input from the moment it is parsed until it lands +somewhere that matters, through every identity and permission decision, every +rewrite, and every place it is stored or copied. Then read the other routes to +the same outcome: older versions, bulk and retry variants, cancellation, +migrations, error handling. Two routes that both have a check may not have the +same check. And where one module hands a value to another, write down what the +first promises and what the second takes for granted. + +Try the unhappy inputs the interface accepts: missing, empty, zero, negative, +maximum, over the limit, duplicated, mixed encodings, stale, revoked, +reordered, concurrent, half-migrated, dependency down, mid-rollback. + +Stay in scope. Trace only paths that reach your boundary or that your boundary +relies on. When a rule is settled either way, record it and move on. If you +find a different boundary that needs attention, report it under "uncovered"; +do not investigate units another hunter owns. + +When a serious candidate reveals a reusable mistake, look for the same mistake +elsewhere in your assigned units. One root cause is one candidate; note each +additional path it affects. + +LOCAL CHECKS + +Run target code only if the lead confirmed a sandbox with: no external +network, an environment built from an explicit allowlist, a read-only target, +writes confined to your scratch directory, and CPU, memory, process, +file-size, disk, and time limits. If any of those is missing, do not run +anything. Return the lead as needs_validation and name the missing control. + +Prefer the narrowest check that settles the question: an existing unit test, +a few-line harness around one function, a small malformed fixture, a dummy +second tenant, a rendered policy. Do not install or download anything. Stop at +the first result that proves or disproves the rule. Never stress a service, +use a real credential, publish anything, or go further than that first result. + +Record the exact input, the command, the limits, and the result. Record only +the environment variable names you set deliberately, never the ambient +environment. Files you want kept as evidence stay in scratch; list their +relative paths in your result and the lead will promote them. + +WHAT YOU CANNOT SEE + +A proxy rule, provider setting, browser behaviour, broker ACL, identity +policy, or network layout that is not in the repository is unknown. Do not +assume it is there and do not assume it is absent. If one such fact decides +the question, return needs_validation naming the exact observation that would +settle it and how the owner can safely make it. +``` + +### Candidate gate — paste into every hunter prompt + +```text +BEFORE YOU REPORT A CANDIDATE + +1. You have a complete trace with repository-relative files and line numbers, + from the entry to the sink, including the strongest control on the way. +2. To propose "confirmed" you also have: a bounded local result you observed, + a consequence that crosses a boundary you can name, every precondition + listed, and no layer in source that prevents it. +3. Report what you saw. A crash is a crash, not code execution. Extra work for + the caller is not an outage. Something a user can do to their own data is + not privilege escalation. +4. A decisive fact you cannot see or reproduce makes the candidate + needs_validation: list the blocker, give no severity. +5. A missing best practice with nobody harmed is a hardening note. A candidate + the source disproves is dropped, not parked as needs_validation. +6. Give each root cause one fingerprint, built from stable source facts + (for example area:control:defect). Letters, digits, and . _ : / @ + - only. + No line numbers, wave, agent name, severity, or verdict. +7. If nothing passes this gate, return an empty candidates array. That is a + valid and useful result. +``` + +## Hunter result format + +```json +{ + "units": [ + { + "coverage_id": "cu-...", + "disposition": "covered | candidate | blocked", + "checks": [ + { + "agent_id": "hunter-03", + "reviewed_paths": ["src/api/orders.py"], + "invariant": "the rule this check tested", + "method": "source | local", + "result": "what the source or the bounded check established", + "scratch_file": null + } + ], + "candidate_fingerprints": [], + "unresolved": [] + } + ], + "candidates": [], + "hardening": ["concrete improvement that is not a finding"], + "uncovered": [ + {"surface": "...", "boundary": "...", "subsystem": "...", + "attack_class": "file.md#Heading", "starting_paths": ["..."], + "reason": "why this needs its own unit"} + ] +} +``` + +Each assigned `coverage_id` appears exactly once. Each entry in `candidates` is +a record shaped like the `confirmed` or `needs_validation` definition in the +schema, with `proposed_verdict` in place of `verdict`. A local check names the +scratch-relative file that holds its output; a source check uses null. + +## Updating the ledger + +The lead is the only writer. + +1. Reject a result that is not a single JSON object, omits an assigned unit, + or names a unit it does not own. Do not repair it. The unit returns to + `planned` for a new hunter. +2. For each local check, run `promote_evidence.py` for the named file. On + success, write the returned path as the check's `evidence_file`. On + rejection, the check is not evidence: if it was decisive, the candidate + becomes `needs_validation` with the rejection as a blocker. +3. Copy the checks into the unit, set `reviewed_paths` to the union of their + paths, and set the status: `covered`, `candidate` with fingerprints, or + `blocked` with the unresolved fact. +4. Keep the hunter's `hardening` notes on the unit for the report. +5. Turn each accepted `uncovered` entry into a new `planned` unit with origin + `new`; derive its ID and re-sort. +6. Merge candidates that share a fingerprint or describe the same underlying + defect. A defect reachable several ways is a single candidate; keep the + most complete trace and list the other routes. +7. Validate the ledger. Increment `agents_spent` in the metadata. + +## The coverage critic + +After each wave, one fresh read-only critic receives `architecture.md`, the +whole ledger, the current fingerprints, and the prior-run gap list. It reads +source and proposes coverage — never findings. It returns: + +```json +{ + "missing_units": [ + {"surface": "...", "boundary": "...", "subsystem": "...", + "attack_class": "file.md#Heading", "starting_paths": ["..."], + "class_blocks": ["file.md#Heading"], "reason": "source-backed gap"} + ], + "reopen": [{"coverage_id": "cu-...", "reason": "why it did not really close"}], + "resolved_prior_leads": ["fingerprint"], + "clean": false +} +``` + +Tell the critic to look for: entry points with no unit, a second route to the +same effect nobody checked, lifecycle modes not represented, companion classes +selected with no unit, exclusions without a source reason, units closed on thin +paths or checks, and prior leads no current unit addresses. + +For each accepted item: + +- **Missing unit** — derive the ID, add it as `planned` with origin `critic`. + Reject proposals outside the scope or the tier; record tier misses as + `out_of_scope`. +- **Reopen** — move the unit's current terminal state into `attempts` exactly + as it stands (`wave`, `status`, `owner`, `reviewed_paths`, `checks`, + `fingerprints`, `unresolved`) and add the critic's `reopen_reason`. Then + increment `wave`, clear the live evidence, and set `planned`. The next owner + must be a new agent, and nothing from the archive is copied forward. + +## When hunting stops + +| Profile | Loop | +|---|---| +| `quick` | One wave, one critic. Accepted missing units and reopens become `deferred` with reason `quick_profile_single_wave`. No second wave. | +| `standard` | Wave, critic, repeat while the critic produces accepted work. When a critic returns clean and nothing is `planned`, a second, different critic must also return clean. | +| `deep` | As `standard`, plus an independent second pass over `recheck_covered` units. | + +Before every wave, check the budget with `audit_run.py budget`: the wave's +critic, the final critic, and the verifier reserve are set aside first, and +hunters get what is left. If the reserves do not fit, launch no hunters; mark +the remaining `planned` units `deferred` and say so in the report. + +Coverage is complete only when the final critic is clean and no unit is +`planned` or `in_progress`. Running out of waves, agents, or time is a reason +to stop, and it is stated as one. It is never evidence that nothing was left. diff --git a/engineering/security-audit/references/memory-safety-and-binary.md b/engineering/security-audit/references/memory-safety-and-binary.md new file mode 100644 index 00000000..3775ea50 --- /dev/null +++ b/engineering/security-audit/references/memory-safety-and-binary.md @@ -0,0 +1,181 @@ +# Memory Safety and Binary Companion + +Tier: full. + +## Select this companion when + +The target contains code where the language does not enforce memory safety or +where bytes are interpreted as structure: C and C++, `unsafe` Rust, manual +memory management in any language, parsers and decoders for untrusted +formats, foreign-function boundaries, binary loaders, just-in-time compilers, +firmware, and kernel or driver interfaces. + +Select it for a boundary reconnaissance found — a parser fed from the network, +an FFI call taking caller-sized buffers — not because a native dependency +appears in a manifest. + +## Ground rules + +```text +- Start from attacker-controlled bytes or sizes and follow them to an + allocation, an index, a copy, a cast, or a free. +- Report the effect you observed and no more: an out-of-bounds read of N + bytes, a write past a buffer, a use after free, a crash. Do not claim code + execution from a crash, and do not build an exploit. +- A sanitizer report or a failing assertion from a bounded local run is good + evidence. Record the exact input and the tool output. +- Undefined behaviour that only the calling program can trigger against + itself is a robustness bug, not a boundary failure. Identify who supplies + the input and whose memory or process is affected. +- Compiler flags, allocator hardening, and platform mitigations reduce + likelihood; they do not make the defect disappear. Note them, do not rely + on them. +- Run fuzzers and harnesses only inside the confirmed sandbox, with small + time and memory limits. A few seconds of targeted input is the goal, not a + campaign. +``` + +## Hunting classes + +### Out-of-bounds read or write + +An index, offset, or length derived from input is used without a check +against the real size, or the check and the use disagree. Look at loop +bounds, off-by-one at terminators, lengths read from the data itself, and +reads that return adjacent memory to the caller. + +### Integer overflow, truncation, and signedness + +Arithmetic on sizes that wraps before allocation or comparison, narrowing +conversions that drop high bits, signed values compared as unsigned or the +reverse, and negative lengths passed to routines expecting sizes. + +### Unit and size confusion + +Bytes mixed with elements, characters with code units, a count used as a +byte length, a size of a pointer used for a size of its target, or a length +that includes a terminator in one place and not another. + +### Uninitialized memory disclosure + +Stack or heap memory returned, serialized, or sent before being fully +written: padding in structures, partially filled buffers, error paths that +skip initialization. + +### Use after free and double free + +An object released while another reference, iterator, callback, or view still +points at it; cleanup paths that free twice; containers resized while a +pointer into them is held; lifetimes that depend on callback ordering. + +### Type confusion + +Memory treated as a type it is not: unchecked downcasts, tagged unions read +with the wrong tag, deserialized type identifiers trusted, variant objects +whose tag and payload can be changed separately. + +### Reference count and ownership races + +Counts incremented or decremented without synchronization, a borrowed +reference used after the owner released it, and ownership transferred across +threads or callbacks with no clear rule. + +### Data races and check-then-use + +Shared state read and acted on without holding the lock that protects it; a +value validated and then re-read; file or object identity checked and then +reopened by name. + +### Deadlock and lock ordering + +Locks taken in inconsistent order, a lock held across a callback or blocking +call that input can stall, and recursive paths that re-acquire. Report as an +availability effect on whoever shares the process. + +### FFI length and ownership contracts + +Each side of a foreign call believes something different about who allocates, +who frees, how long a pointer is valid, or how large a buffer is. Read the +declaration on both sides and the wrapper between them. + +### Layout, alignment, and enum mismatch + +Structures declared differently across languages or compilation units, +packing and alignment assumptions, and enum values outside the range the +receiving side handles. + +### Unwinding across a foreign boundary + +Exceptions or panics that cross into code that cannot handle them, leaving +locks held or state half-updated, and callbacks invoked on a thread the +callee does not expect. + +### Library and plugin search order + +The process loads a library, plugin, or helper executable by name from a +search path that includes a directory a lower-privilege user can write to. + +### Unverified loaded artifacts + +Code, modules, or firmware images loaded with no check of signature or +digest, or with a check on one file while a different file is executed. + +### Malformed binary metadata + +Loaders and format parsers that trust offsets, counts, section sizes, and +relocation entries inside the file: overlapping sections, offsets beyond the +end, counts that overflow when multiplied. + +### JIT and generated code consistency + +Generated code that assumes a type or bound the runtime can later invalidate, +writable-and-executable regions, and caches of compiled code keyed too +loosely. + +### Unload and teardown + +Callbacks, timers, or threads that still reference a module or object being +unloaded; shutdown paths that free state other threads are using. + +### User-copy bounds and double fetch + +Kernel or privileged code copying from a caller: sizes taken from the caller +without a ceiling, a value fetched twice with a check in between, and +structures copied back with uninitialized fields. + +### Privileged object lifetime + +Handles, descriptors, and kernel objects whose lifetime a caller can race: +closing during an operation, reusing an identifier after release, dispatch +tables indexed by a caller-supplied number. + +### Powerful interfaces with weak checks + +Device controls, debug interfaces, and management calls that perform +privileged operations and verify only that the caller could open the device. + +## Cross-cutting moves + +- For every length, find where it comes from, where it is checked, and where + it is used. All three must agree on unit and type. +- Read error and early-return paths for skipped initialization and double + cleanup. +- Check the largest and smallest values: zero, one, maximum, maximum plus + one, negative. +- Follow each allocation to every free and each free to every remaining + reference. +- Prefer an existing unit test or fuzz target as the harness, with a single + crafted input. + +## Evidence bar + +- Confirmed needs a concrete input and an observed effect from a bounded + local run: a sanitizer report, a failed bounds assertion, or adjacent + memory in the output. +- If no sandbox or toolchain is available, a fully traced arithmetic or + lifetime argument stays `needs_validation`, with the missing run as the + blocker and the exact harness described. +- Severity follows the observed primitive and who reaches it: a remote + unauthenticated write is high or critical; a bounded read of non-sensitive + memory is low. Do not rate on what an exploit might achieve. +- Crashes only the caller can inflict on itself are hardening notes. diff --git a/engineering/security-audit/references/protocols-rpc-and-messaging.md b/engineering/security-audit/references/protocols-rpc-and-messaging.md new file mode 100644 index 00000000..fe8f28af --- /dev/null +++ b/engineering/security-audit/references/protocols-rpc-and-messaging.md @@ -0,0 +1,141 @@ +# Protocols, RPC, and Messaging Companion + +Tier: full. + +## Select this companion when + +Components talk to each other through something other than plain +request-response HTTP handled by a framework: gRPC and other RPC systems, +schema-based serialization, hand-written wire protocols, message queues and +brokers, publish-subscribe topics, webhooks, and long-lived or streaming +connections. + +These paths often sit "inside" the network and inherit less scrutiny than +public endpoints, while carrying the same authority. + +## Ground rules + +```text +- Every message has a producer, a channel, and a consumer. Establish who can + act as the producer and what the consumer assumes about it. +- A channel being internal is a deployment fact, not a control visible in + source. If safety rests on network placement or broker access rules that + are not in the repository, the lead is needs_validation. +- Identity carried inside a message is a claim. Identity established by the + channel is evidence. Find where the code confuses them. +- Delivery is at-least-once, unordered, and delayed unless the source proves + otherwise. Ask what happens on duplicate, reordered, and stale messages. +- Reproduce with an in-process or loopback fixture: a fake producer, the real + consumer code, dummy tenants. Never publish to or read from a shared broker. +``` + +## Hunting classes + +### Message boundary and canonical form disagreement + +Two parties parse the same bytes differently: length prefixes versus +delimiters, duplicate fields, unknown fields kept or dropped, different +canonical encodings of the same value, a signature computed over one form and +a decision made on another. + +### Union, enum, and default handling + +Unset fields that default to a permissive value, unknown enum values mapped +to a privileged or pass-through case, one-of fields where two alternatives +can both be populated, and version skew between producer and consumer +schemas. + +### Envelope and payload identity mismatch + +Routing, tenant, or sender information appears in both the envelope and the +body, and the authorization check reads one while the handler acts on the +other. + +### Interceptor and method coverage gaps + +Authentication or authorization applied by middleware that does not cover +every method: newly added methods, streaming variants, reflection and health +services, a second server instance registered without the interceptor. + +### Peer identity mapped to application principal + +A transport-level identity — a service certificate, a connection credential — +is treated as permission to act for any user the message names. Check whether +the callee validates that this peer may speak for that principal. + +### Per-item and streaming authorization + +Authorization done once at stream open or batch start and not for each item, +resource, or subscription change that follows; long-lived streams that +outlast a revoked permission. + +### Callback and reply correlation + +Replies matched to requests by a guessable or reusable identifier, reply +destinations chosen by the requester without restriction, and callbacks +delivered to an address the sender supplied. + +### Topic and subscription scope + +Tenants or roles able to subscribe to, or publish on, topics beyond their +own: wildcard subscriptions, topic names built from caller input, shared +consumer groups, per-tenant data on a common topic with filtering left to the +consumer. + +### Dead-letter, retry, and diagnostic leakage + +Failed messages, with their full payloads, parked where a wider audience can +read them, or replayed later under different authorization. Includes +diagnostic and tracing topics. + +### Producer treated as control plane + +Consumers that execute commands, change configuration, or grant access based +on a message, on a channel where less-trusted producers can also publish. + +### Duplicate delivery and idempotency + +Handlers that apply an effect each time a message arrives: payments, +grants, counters, notifications. Check for a deduplication key, where it is +stored, and whether it is chosen by the sender. + +### Stale and out-of-order messages + +An older message applied after a newer one, undoing a revocation or restoring +a previous state; missing version or sequence checks; timestamps trusted from +the producer. + +### Acknowledge and commit ordering + +A message acknowledged before its effect is durable, or an effect committed +without the acknowledgment, so a crash loses or repeats a security-relevant +step. + +### Partial fan-out transitions + +One event consumed by several services, where some apply the change and +others fail, leaving access granted in one place and revoked in another. + +## Cross-cutting moves + +- For each channel, write who can produce, who can consume, and what the code + — not the network — enforces about each. +- Send the consumer a message its own producer would never send: extra + fields, missing fields, wrong tenant, unknown type. +- Deliver every message twice, then in reverse order. +- Check webhook receivers for signature verification, replay windows, and + which bytes the signature covers. +- Compare the public API's checks with those on the internal RPC that does + the same thing. + +## Evidence bar + +- Confirmed needs a local fixture run: the crafted message, the consumer's + code path, and the wrong effect on a dummy tenant or resource. +- Whether a lower-trust actor can actually publish to the channel is often a + broker or network fact. If it is not in the repository, record + `needs_validation` with an owner check for the exact access rule. +- Missing encryption or authentication on a channel that the source shows to + be loopback-only is a hardening note. +- Severity follows the consumer's authority: a message that grants access or + moves value is high; a duplicated notification is low. diff --git a/engineering/security-audit/references/reconnaissance.md b/engineering/security-audit/references/reconnaissance.md new file mode 100644 index 00000000..2188a7fe --- /dev/null +++ b/engineering/security-audit/references/reconnaissance.md @@ -0,0 +1,239 @@ +# Phase 1 — Reconnaissance and the Coverage Ledger + +Reconnaissance produces two things: a short architecture summary every later +agent reads, and a coverage ledger that says exactly what the audit intends to +examine. Nothing is hunted until both exist and the ledger validates. + +Reconnaissance is read-only. Scouts read the repository and whatever build and +configuration state is already on disk. They do not run the target, install +anything, or contact a deployed service, registry, identity provider, or cloud +API. + +## Before launching scouts + +1. Run `audit_run.py init` (see SKILL.md). It creates the run directory outside + the target, records tier, profile, scope, and budget in + `run-metadata.json`, and refuses a budget too small to fund reconnaissance, + critics, and one verifier. +2. Read the `prior_runs` it reports. Prior ledgers and findings change the plan + (see "Using prior runs"). + +## Scout assignments + +Launch four read-only scouts in parallel. Each reports facts, cites the file +and line for every one using paths relative to the repository root, and writes +no files. + +### Scout A — what the product is and how it runs + +```text +Read the repository at . Do not use the network and do not run anything. +Report, with file:line references: +1. What the product is, who uses it, who operates it, and which everyday + actions carry trust (login, payment, sharing, admin, publish). +2. The technology in use and how it is built, plus each way of deploying it + that the repository itself describes. +3. Entry points and the subsystems behind them. +4. Build and test commands that would work offline with what is already + installed, where they write, and which target-controlled inputs they read. +5. Whether local documentation or dependencies point to a well-known + comparable system. Say "none" when nothing in the source supports one. +6. Toolchains or runtime facts that are missing and would limit local checks. +Facts only. No findings, no recommendations. +``` + +### Scout B — principals, authority, and controls + +```text +Read every part of the repository that decides who someone is and what they +may do. Report, with file:line references: +1. Each kind of principal, from least to most trusted, and what each can do + by design. +2. How identity is established at each entry surface. +3. Where per-object and per-tenant decisions are made. +4. Authority held by non-human actors: processes, workloads, CI jobs, + plugins, model-driven tools, devices, local helpers. +5. Paths that change privilege: elevation, confirmation, revocation, + recovery, fallback. +6. For each control, whether it is visible in source or depends on a + deployment fact the repository does not contain. +Do not infer what a live deployment does. +``` + +### Scout C — entry surfaces, copies, and sinks + +```text +List every place lower-trust input enters the system: HTTP and browser, RPC +and messages, files and archives, CLI arguments, environment and config, +plugins and dependencies, CI inputs, cloud events, model context and tool +arguments, mobile and deep links, local IPC. +For each, follow the input through its main transformations, note every +stored or derived copy (cache, index, log, export, queue), and name the +security-relevant sinks it reaches. Note limits the source enforces and any +second route to the same effect. +Give file:line references. Be complete. Do not send or execute any input. +``` + +### Scout D — what can run locally and what only a deployment knows + +```text +Read tests, build files, manifests, packaging, and environment overlays. +Report, with file:line references: +1. Existing offline tests or fixtures that could exercise a trust boundary + with dummy data. +2. Processes that could run against loopback only, with no shared dependency. +3. Commands that fetch dependencies, publish, call paid or provider APIs, or + touch shared state. These are prohibited for this run. +4. Controls that only a deployment can establish (proxy rules, provider + settings, identity policy, network topology). +5. Which code path each deployment mode selects, where the repository makes + that deterministic. +6. Whether this machine can enforce the sandbox the audit requires: no + external network, an allowlisted environment, a read-only target, + scratch-only writes, and CPU, memory, process, file-size, disk, and time + limits. State each control as available or missing. +``` + +Add a focused scout for a subsystem or deployment mode these four leave +unmapped. If the budget cannot pay for it, do not drop the area: seed it as a +`deferred` unit with the reason and disclose it in the report. + +## Using prior runs + +A second run is expected, not exceptional — independent passes surface +different defects. Read every prior `coverage-ledger.json` and `findings.json` +for the same repository and compare them with the current source, not with the +recorded commit hash alone. + +| Prior state | Current source | What to seed | `origin` | +|---|---|---|---| +| `confirmed` record | Path and conditions unchanged | A `planned` unit; the record goes to a fresh verifier, hunters are told to skip that root cause | `carried_confirmed` | +| `confirmed` record | Anything relevant changed | A `planned` unit that hunters work normally | `revalidate_confirmed` | +| `needs_validation` record | Trace still holds | A `planned` unit; the record goes to a fresh verifier with its blocker intact | `carried_needs_validation` | +| `deferred`, `blocked`, or `out_of_scope` unit | Any | Current `planned` work if now in scope | `reopened_gap` | +| `covered` unit | Unchanged | Keep visible; assign after changed and gap units | `recheck_covered` | +| `rejected` record | Unchanged | Suppresses that exact claim only; the unit is still reviewed | — | +| Surface not in any prior ledger | — | A `planned` unit | `new` | + +With no prior ledger, every unit has origin `none`. A prior `quick` or scoped +run contributes the evidence it recorded and nothing more — never read it as +"the rest was fine". Record unreadable or incompatible prior files in +`run-metadata.json` rather than treating them as empty. + +## The architecture summary + +The lead writes `architecture.md` from the scouts' facts. Hard limit: about +1,000 words. It is pasted into every hunter prompt, so every sentence costs +tokens many times over. + +1. Product, principals, what each may normally do, and what is protected. +2. Stack, deployment shapes visible in source, and what can run offline. +3. Entry surfaces and the important paths from input to sink. +4. Trust boundaries, each with the strongest control visible in source. +5. Starting paths, repository-relative. +6. A comparable system and the trade-offs it accepts, only when the source + supports the comparison. Use it to calibrate, never to wave a finding away. +7. Prior-run consequences: gaps reopened, records carried, root causes hunters + must skip. +8. The companions selected, each with the boundary that justified it, and any + boundary left out because its companion is outside the tier. + +Select a companion because a scout found the boundary it describes, not +because a framework or language name appears in the dependency list. + +## Seeding the ledger + +`coverage-ledger.json` is `{"schema_version": 1, "run_id": "...", "units": []}`. +Seed one unit for each material combination of **surface**, **boundary**, +**subsystem**, and **attack class**. + +```json +{ + "coverage_id": "cu-c565fdde20a55cc15828", + "refs": { + "surface": "src/api/orders.py#POST /orders/{id}/refund", + "boundary": "src/authz/policy.py#require_owner", + "subsystem": "services/orders", + "attack_class": "attack-classes.md#Access control" + }, + "label": "Refund endpoint: owner check on order refunds", + "starting_paths": ["src/api/orders.py", "src/authz/policy.py"], + "class_blocks": [ + "attack-classes.md#Access control", + "data-isolation-and-lifecycle.md#Missing tenant or owner scope" + ], + "excluded_blocks": [ + {"block": "attack-classes.md#Injection", + "reason": "handler takes a numeric ID and a fixed enum; no interpreted sink"} + ], + "origin": "none", + "wave": 1, + "status": "planned", + "owner": null, + "reviewed_paths": [], + "checks": [], + "fingerprints": [], + "unresolved": [], + "attempts": [] +} +``` + +Rules for the fields: + +- **`refs`** are source-derived and stable: a path plus the route, message + name, or symbol defined in source; the control that defines the boundary; a + package path; the exact block reference. Never a display name, line number, + wave, agent, or verdict. The same source object gets the same reference in + every run. Add `refs.lifecycle` only when a lifecycle mode (setup, migration, + restore) is a materially different unit. +- **`coverage_id`** is derived from `refs`, never invented: + `audit_run.py coverage-id --surface ... --boundary ... --subsystem ... + --attack-class ...`. Identical refs give an identical ID, which is how + duplicates and cross-run matches are detected. +- **`class_blocks`** is the exact set of blocks the hunter will be given: + `refs.attack_class` first, then any companion classes. Whenever a companion + class is listed, the hunter also receives that companion's `Ground rules`, + `Cross-cutting moves`, and `Evidence bar` sections. +- **`excluded_blocks`** records each block considered and ruled out, with the + source fact behind the decision. An unexplained absence is a coverage gap + the critic will raise. +- Units are sorted by `coverage_id`. + +Granularity follows the profile: `quick` uses one subsystem reference, +`profile/quick/all`, for everything in scope; `standard` splits by subsystem; +`deep` also splits by lifecycle mode. When the run has a scope, only surfaces +inside it become `planned`; anything else the scouts noticed is still written +down, as `out_of_scope` with a reason. + +## Unit states + +| Status | Owner | Paths and checks | Fingerprints | `unresolved` | +|---|---|---|---|---| +| `planned` | none | empty | empty | empty | +| `in_progress` | agent ID | empty | empty | empty | +| `covered` | agent ID | filled | empty | empty | +| `candidate` | agent ID | filled | filled | optional | +| `blocked` | agent ID | filled (partial review) | empty | the blocker | +| `deferred`, `out_of_scope`, `not_applicable` | none | empty | empty | the reason | + +`python3 scripts/ledger_rules.py` prints this table. Agent IDs match +`^[a-z0-9][a-z0-9_-]{0,63}$`. + +Each check names its own `agent_id` and the `reviewed_paths` it covered; the +unit's `reviewed_paths` is the union. A `source` check has +`evidence_file: null`. A `local` check points at a file under +`agents//evidence/`, put there by `promote_evidence.py` and nothing +else. A hunter's check and a verifier's check can sit in the same unit, each +owned by its author. + +## Validate before assigning work + +```bash +python3 scripts/validate_coverage_ledger.py /coverage-ledger.json \ + --metadata /run-metadata.json +``` + +Run it after seeding and after every later edit. A ledger that fails cannot +drive an assignment or support a coverage claim. The ledger — not the +architecture summary, the number of agents launched, or a sentence saying +"authentication was reviewed" — is the record of what the audit covered. diff --git a/engineering/security-audit/references/resource-exhaustion-and-availability.md b/engineering/security-audit/references/resource-exhaustion-and-availability.md new file mode 100644 index 00000000..5a090e0c --- /dev/null +++ b/engineering/security-audit/references/resource-exhaustion-and-availability.md @@ -0,0 +1,141 @@ +# Resource Exhaustion and Availability Companion + +Tier: full. + +## Select this companion when + +Work requested by a lower-trust actor consumes something shared: CPU, memory, +disk, connections, worker threads, queue capacity, rate or quota allowances, +or money the operator pays for (compute, storage, third-party API calls). + +Availability findings are easy to overstate. This file exists to keep them +honest: a cheap request, a disproportionate cost, and somebody other than the +requester who pays. + +## Ground rules + +```text +- A finding needs asymmetry and a victim. Show that a small input causes + large work, and that the cost lands on other users, a shared service, or + the operator. +- Reason from source: complexity, bounds, limits, and timeouts. Demonstrate + with one small input and a measurement, inside the sandbox, with strict + CPU, memory, and time caps. Scale by argument, not by load. +- Never send volume at anything. No stress tests, no floods, no attempts to + exhaust a real or shared process, and nothing that incurs spend. +- Rate limits, quotas, autoscaling ceilings, and upstream timeouts set + outside the repository are unknown. If one of them would contain the + effect, the lead is needs_validation with that limit as the blocker. +- "No rate limit" is a hardening note until you tie it to a specific + expensive operation and a specific shared resource. +- Slowness or failure that affects only the requester's own session is not a + finding. +``` + +## Hunting classes + +### Superlinear parsing and matching + +Input that drives quadratic or exponential work: backtracking regular +expressions on caller text, nested structures parsed recursively, repeated +string concatenation or rescans, sort and comparison functions on crafted +keys, hash collisions. + +### Decompression and expansion + +Small input that expands enormously: compressed uploads and request bodies, +archives with many or huge entries, image and document formats with declared +dimensions, entity or reference expansion, templates and macros. + +### Query and downstream amplification + +One request that causes many or expensive downstream operations: unbounded +page sizes, deep or wide graph queries, filters that defeat indexes, fan-out +to other services, per-item calls inside a loop over caller-supplied lists. + +### Unbounded buffering and cardinality + +Data accumulated in memory or storage with no cap: request bodies, streaming +uploads read whole, in-memory caches and maps keyed by caller input, metric +labels and log fields with unbounded distinct values, per-user objects with +no count limit. + +### Handle and temporary resource leaks + +Descriptors, connections, locks, temporary files, or sessions acquired per +request and not released on error, timeout, or disconnect. + +### Work that survives cancellation + +A client disconnects or a request times out and the expensive work +continues, so cost can be started cheaply and repeatedly. + +### Expensive work before authentication + +Costly operations reachable before the caller is identified or limited: +password hashing on arbitrary input, signature verification of large +payloads, upload processing, account lookups, report generation behind an +unauthenticated link. + +### Quota scope and reset gaps + +Limits keyed on something the caller controls or can rotate — an address +header, an account they can recreate, a key they can mint — limits applied on +one route to a resource and missing on another, and counters that reset or +are not shared across instances. + +### Worker and pool starvation + +A small number of slow or blocking requests occupying a shared pool: sync +calls on an async loop, requests that hold a database connection while +waiting on something slow, one tenant's jobs ahead of everyone's in a single +queue. + +### Reachable crash or deadlock + +Input that terminates or hangs a process serving others: uncaught exceptions +in shared workers, assertion failures, stack exhaustion, lock cycles. +Establish that the process is shared and what restarts it. + +### Retry storms and fail-open + +Failure handling that multiplies load — unbounded or synchronized retries, +retries at several layers — or that drops a protection when a dependency is +slow: a limiter, a validator, or an authorization cache that allows on +timeout. + +### Poison messages and head-of-line blocking + +A message or job that always fails and is always retried, blocking the +partition or queue behind it, with no cap on attempts and no quarantine. + +### Unsafe recovery + +Restart, replay, or cache-warm procedures that reprocess attacker-supplied +input, re-trigger the original fault, or come up with limits and protections +not yet in force. + +## Cross-cutting moves + +- For each expensive operation, write the cost as a function of something the + caller controls, then look for the bound on that input. +- Find every limit and ask what it is keyed on and where it is enforced. +- Check the timeout at each hop, and whether the work stops when it fires. +- Look for the shared thing: one pool, one queue, one lock, one process, one + bill. +- Measure once with a small input and a doubled input; the ratio is the + evidence. + +## Evidence bar + +- Confirmed needs a source-level bound analysis plus one bounded local + measurement showing the asymmetry — for example, input doubled and time + quadrupled — and a shared resource named in source. +- Without a sandbox, or where an external limit might contain the effect, + record `needs_validation` with the limit or the measurement as the blocker. +- Severity is capped by what was shown. An unauthenticated request that stops + a shared service can be high; degraded latency for a single tenant, or cost + that scales linearly with the requester's own usage, is low or a hardening + note. +- Never rate availability findings on an untested claim that "enough + requests" would take the system down. diff --git a/engineering/security-audit/references/supply-chain-and-release.md b/engineering/security-audit/references/supply-chain-and-release.md new file mode 100644 index 00000000..e4827a39 --- /dev/null +++ b/engineering/security-audit/references/supply-chain-and-release.md @@ -0,0 +1,141 @@ +# Supply Chain and Release Companion + +Tier: focused, full. + +## Select this companion when + +The repository contains the machinery that turns source into something +shipped: dependency manifests and resolvers, code generators, CI workflow +definitions, build scripts, container build files, release and signing steps, +auto-update logic, or a plugin and extension system. + +The question throughout is who can change what ends up running, and with whose +authority. Vulnerabilities inside third-party packages are a dependency-audit +matter; this file is about the target's own handling of inputs and authority. + +## Ground rules + +```text +- Read workflow, build, and release definitions as code that runs with + credentials. Identify the trigger, who can cause it, what it checks out, + and which secrets and permissions are present at each step. +- A finding needs an actor (outside contributor, low-privilege maintainer, + compromised dependency, plugin author), the step they influence, and what + they gain: code in a release, a secret, write access, a signed artifact. +- Do not run workflows, publish, sign, or contact a registry. Evidence comes + from the definitions in the repository and from rendering or linting them + locally. +- Repository settings, branch protection, environment approvals, and registry + configuration that are not in source are unknown. If the defect depends on + one, it is needs_validation with that setting as the blocker. +- An unpinned version is a hardening note unless you can show the path from + that mutability to code running with authority. +``` + +## Hunting classes + +### Dependency source and name confusion + +The resolver can be made to fetch a package from an unintended source: an +internal name resolvable from a public registry, several indexes with no +priority rule, scoped names missing a registry mapping, a typo-adjacent name, +or a dependency pulled from a branch or URL someone else controls. + +### Mutable build inputs + +Anything fetched at build time by a reference that can change: tags, branches, +`latest`, unpinned base images, install scripts piped from a URL, actions or +shared pipeline steps referenced by a moving tag, lockfiles absent or ignored +by the install command actually used. + +### Generated code without provenance + +Checked-in generated files, vendored copies, or minified bundles that the +build does not regenerate or verify, so reviewed source and shipped code can +differ. Also generators that run with network or file access during the build. + +### Build context leakage + +Secrets, credentials, version-control metadata, or local config copied into an +image layer, an artifact, a source map, or a published package because ignore +rules or copy steps are too broad. + +### Outsider code running with pipeline authority + +A workflow that runs on events an outsider can trigger and both checks out +their code and holds secrets or write permission. Look at each trigger type, +what ref is checked out, and whether install or test steps execute +contributor-controlled scripts before any gate. + +### Workflow expression and command injection + +Event fields an outsider controls — titles, branch names, labels, comment +bodies, file names — interpolated into a shell step, a script, or an +environment file. The fix is passing them as data, so the finding is the +interpolation into an interpreter. + +### Shared caches, artifacts, and workspaces + +Caches or artifacts written by a low-trust job and read by a privileged one; +cache keys an outsider can predict or collide; self-hosted runners that keep +state between jobs from different trust levels. + +### Over-privileged automation identity + +Tokens, deploy keys, or cloud roles granted to a job that needs less: +repository-wide write for a lint job, long-lived secrets where a short-lived +identity exists, one credential shared across environments. + +### Artifact substitution between build and release + +The thing tested is not provably the thing published: rebuild at release +time, promotion by mutable name, artifacts fetched without digest +verification, a gap between stages where a lower-trust job can replace the +file. + +### Release approval and signing gaps + +Who can cut a release, and is that enforced in the pipeline definition? Signing +keys reachable from ordinary jobs, signing that happens before checks finish, +signatures produced but never verified by the consumer, provenance not bound +to the source commit. + +### Update metadata and rollback + +For a self-updating client: update manifests fetched without authentication of +their content, version comparison that accepts an older release, a signature +checked on the package but not on the metadata that selects it, and fallback +to an unverified channel on error. + +### Plugin and extension privilege + +Third-party code loaded into the product: what it can reach by default, how +its identity and version are verified, whether an update can widen its +permissions without consent, and whether one plugin can affect another or the +host's secrets. + +## Cross-cutting moves + +- For each job, list trigger, actor, checked-out ref, secrets in scope, and + token permissions. The dangerous combination is outsider-triggered, with + outsider code, with authority. +- Follow one artifact from commit to consumer and note every point where it is + identified by name instead of by digest. +- Check the unhappy paths of verification: missing signature, unreachable key + server, expired certificate. +- Compare the documented release process with what the definitions enforce. +- Look at composite steps, reusable workflows, and scripts called from the + pipeline; the injection is often one level down. + +## Evidence bar + +- Confirmed needs the definition text that grants the authority, the input the + actor controls, and a local demonstration where one is possible: the + rendered command line, a linter or dry-run result, a resolver run against + local fixtures. +- Anything that depends on hosted settings not present in the repository is + `needs_validation`, with an owner check naming the exact setting. +- Never demonstrate by triggering a real pipeline, publishing a package, or + registering a name. +- Severity reflects what the actor obtains: release integrity and signing + authority rank above a read-only secret, which ranks above a cache nuisance. diff --git a/engineering/security-audit/references/validation-and-reporting.md b/engineering/security-audit/references/validation-and-reporting.md new file mode 100644 index 00000000..e40565c2 --- /dev/null +++ b/engineering/security-audit/references/validation-and-reporting.md @@ -0,0 +1,220 @@ +# Phases 3–6 — Validation, Records, Verification, Reporting + +A hunter's candidate is a claim. It becomes a finding only after an agent that +did not write it has tried to break it, the record has passed the validators, +and a second fresh reader has checked the record against the source. + +## Phase 3 — Try to refute every candidate + +Give each unique candidate — both proposed `confirmed` and proposed +`needs_validation`, plus every record carried from a prior run — to a fresh +verifier that took no part in hunting it. One candidate per verifier. + +The verifier receives: the candidate, the checks and evidence files from its +linked units, the parts of `architecture.md` needed to read the path, the +`Evidence bar` of each companion involved, the three record definitions from +`assets/findings.schema.json`, and any prior record with the same fingerprint. +It does not receive another verifier's opinion. + +### Verifier prompt + +```text +Someone else produced the candidate below. Your job is to disprove it using +the repository and, where permitted, a small local check. If you cannot disprove +it, say exactly what it does and does not establish. + +Do not contact any deployed or shared system. Run target code only inside the +sandbox the lead confirmed (no external network, allowlisted environment, +read-only target, scratch-only writes, resource and time limits). If the +sandbox is not available, do not run anything; that missing control becomes a +blocker. + +1. Open every file and line the candidate cites. Is the entry really reachable + by the stated actor? Is the sink really where the trace says it ends? +2. Look for whatever would stop this: input checks, identity and permission + decisions, canonicalization, protections the framework applies by default, + sandboxing. The hunter may have missed one. +3. For a proposed confirmed candidate, reproduce the smallest result yourself + when you safely can. Check the inputs, the preconditions, and who or what + is affected. Do not go beyond that result. +4. Check that the ratings and the suggested fix claim no more than the + evidence shows. +5. Where the candidate is offered as needs_validation, test the blocker: can + the source or a local check really not settle it? If the source disproves the + trace, reject. If the fact is still decisive and still unknown, keep it and + make the resolution steps exact and harmless. +6. Keep the fingerprint unless you found a different root cause. + +Return exactly one JSON object, nothing else: +{"decision": "confirmed | needs_validation | rejected", "record": { ... }} +The record must match that decision's definition in the schema. Your record +replaces the hunter's wording. +``` + +### Deciding + +- **Promote** `needs_validation` to `confirmed` only when the verifier itself + established the whole path and a bounded observed result. +- **Demote** `confirmed` to `needs_validation` when one specific deployment or + runtime fact is still unknown. +- **Reject** when the source, local behaviour, a visible control, an absent + consequence, or an impossible precondition refutes the claim. + +`needs_validation` is for a specific, source-grounded hypothesis with a named +missing fact. It is not a shelf for ideas nobody could support. + +The lead then: promotes any evidence the verifier produced, adds the +verifier's check to the linked unit under the verifier's own `agent_id` +(the unit keeps its original owner), and keeps one final record per +fingerprint. For a carried record whose unit was seeded `planned`, the +verifier becomes the unit's owner and its re-check moves the unit to +`candidate`. If a verifier returns anything other than the single JSON object, throw the +result away and give the candidate to another fresh verifier. + +When there are more candidates than the budget can verify, hunting ends. +Verify as many as the budget allows, taking them in fingerprint order, and set +`run_status` to `incomplete` +with `incomplete_reason: "validation_budget_exhausted"`. An unvalidated +candidate stays in the ledger only. It never enters `findings.json` under any +verdict. + +## Phase 4 — Records + +The lead writes every decided record, sorted by fingerprint, into +`{"schema_version": 1, "run_id": "...", "findings": [...]}`. The verdict +decides the fields; the schema rejects anything else. + +| Field | `confirmed` | `needs_validation` | `rejected` | +|---|---|---|---| +| `fingerprint`, `title`, `summary`, `trace`, `evidence` | required | required | required | +| `root_cause`, `violated_invariant`, `actor`, `affected` | required | — | — | +| `preconditions`, `reproduction`, `fix`, `severity`, `confidence` | required | — | — | +| `suspected_root_cause` | — | required | required | +| `blockers`, `resolution` (`local` and/or `owner_check`) | — | required | — | +| `rejection_reason` | — | — | required | + +Notes on the fields that carry the weight: + +- **`trace`** — ordered steps of `entry`, `flow`, `sink`. A multi-step trace + starts with `entry` and ends with `sink`. Files are repository-relative. +- **`reproduction`** — written in the target's own interface, which is not + always HTTP: a function call for a library, a fixture for a parser, a command + for a CLI, a message for a consumer, a rendered policy for infrastructure. + `inputs` holds the minimum test input. `observed` is what actually happened + locally, stated plainly. +- **`fix`** — the rule the code must uphold, the smallest edit that makes it + hold at the point where the decision is actually taken, and the regression + test that would have caught it. The audit describes the fix; it does not edit the target. +- **`severity`** — likelihood and impact rated separately with reasons. + `overall` may not exceed `impact`. A `needs_validation` record has no + severity at all. +- **`resolution.owner_check`** — something the system's owner observes in + their own environment (a config value, a route, a policy). It is never an + instruction to probe a live target. + +Rejected records are kept so the next run does not raise the same unsupported +claim again without new evidence. + +```bash +python3 scripts/validate_findings.py /findings.json +python3 scripts/validate_coverage_ledger.py /coverage-ledger.json \ + --metadata /run-metadata.json \ + --findings /findings.json --run-dir +``` + +Fix every error before continuing. Passing means the records are well formed +and consistent with the ledger. It does not mean they are true — that is +Phase 5. + +## Phase 5 — Check the final records with fresh eyes + +Every `confirmed` and `needs_validation` record now gets its own new +read-only reader; these can run side by side. Each reads the record as written, +not the hunter's notes. + +For a `confirmed` record it verifies: every cited path and line; that the +entry interface and input shape are real; each precondition and each control +on the path; who is affected and that the stated impact was demonstrated; that +severity does not exceed impact; and that the fix enforces the invariant +instead of moving the trust somewhere else. + +For a `needs_validation` record it verifies: the cited code exists and backs +the suspected root cause without overreaching; each blocker is decisive and +cannot be settled locally; the lead names a boundary and a concrete possible +result; and the resolution steps are exact and harmless. + +It returns one JSON object: `{"outcome": "accepted", "fingerprint": "..."}` or +`{"outcome": "revise", "why": "...", "record": {...}}`. + +A replacement is **material** if it raises the verdict, or changes the root +cause, trace, reproduction input or result, demonstrated impact, or severity. +A material replacement is not applied directly. It goes to another fresh +verifier — one that did not hunt, validate, or propose it — and is applied only +when that verifier returns `accepted`. If independence or budget runs out, +the contested record comes out of `findings.json`, its unit stays an +unresolved candidate, and mark the run `incomplete` with the reason. Wording +and line-number corrections that change no meaning may be applied directly. + +Re-run both validators after every applied replacement. In a `quick` run, +Phases 3 and 5 are done by the same single fresh verifier per candidate; every +other profile keeps them separate. No profile skips independent review of a +`confirmed` record, and unresolved leads are verified too — a misleading lead +costs the owner real time. + +Set `run_status: "complete"` only when every candidate in the ledger has a +final record and every retained record has passed this phase. + +## Phase 6 — Write the reports + +Run the final gate first: + +```bash +python3 scripts/validate_coverage_ledger.py /coverage-ledger.json \ + --metadata /run-metadata.json \ + --findings /findings.json --run-dir --final +python3 scripts/audit_run.py summary --run-dir +``` + +The reports are derived from the records, the ledger, and the retained +hardening notes. Prose never changes a verdict, a severity, a blocker, or a +demonstrated impact. `assets/report_template.md` gives the layout. + +### `REPORT.md` + +1. **Run facts** — tier, profile, scope, source ref, budget and agents spent, + prior runs used, and the statement that evidence came from source review + and sandboxed local checks only. A `core` or `focused` tier, a `quick` + profile, a scoped run, a budget-limited run, or an incomplete run says in + the first paragraph that it is a partial pass and why. +2. **Posture** — one short paragraph. +3. **Confirmed findings table** — severity, title, boundary, observed result. +4. **Each confirmed finding** — location, actor, bounded reproduction, + preconditions, result, impact, why it has this priority, smallest fix. +5. **Needs validation** — a separate table: title, trace, exact blocker, local + next step, owner check. No severity, and never described as a vulnerability. +6. **Hardening notes and good patterns found.** +7. **Coverage** — the `audit_run.py summary` tables: unit counts by status, + every deferred, blocked, and out-of-scope unit with its reason, and the + final critic's result. + +Rejected records are not findings. Mention a rejected fingerprint only to +explain a coverage decision. + +### `FINDINGS-DETAIL.md` + +For each `confirmed` record rated medium or above: the full trace and +evidence, the dummy actor and the dummy resource affected, the exact input and +steps, the observed output and the rule it breaks, the preconditions, and the +fix with its regression test. + +### `NEEDS-VALIDATION.md` + +For each unresolved record: the trace, the verified evidence, the exact +blocker, the boundary at stake, and each resolution step. These are leads +ranked for follow-up. They carry no severity and include no instructions for +testing a live system. + +### Proportion + +A run with no confirmed findings says so, states what was and was not covered, +and stops. Do not manufacture low-severity findings to fill a table. diff --git a/engineering/security-audit/references/web-protocol-and-auth.md b/engineering/security-audit/references/web-protocol-and-auth.md new file mode 100644 index 00000000..c489673b --- /dev/null +++ b/engineering/security-audit/references/web-protocol-and-auth.md @@ -0,0 +1,185 @@ +# Web Protocol and Authentication Companion + +Tier: focused, full. + +## Select this companion when + +Reconnaissance found code that parses or forwards HTTP itself, sits behind or +acts as a proxy, cache, CDN, or gateway, or implements sign-in: sessions, +cookies, CSRF defences, JWT, OAuth or OIDC, SAML, MFA, passkeys, recovery, +account linking, API keys, or client certificates. + +Use it with the core `Access control` and `Injection` classes, not instead of +them. This file covers how identity is established and carried; the core +classes cover what an established identity may do. Split large targets into +separate units for protocol handling, session, federation, and recovery. + +## Ground rules + +```text +- An absent header, flag, or recommended setting proves nothing alone. + Show the request an attacker sends, the victim or resource affected, and the + result. +- Identity must be bound at every hop. Name what binds the credential to the + user, to the client, to the request, and to the intended recipient, and show + which binding is absent. +- Behaviour of a proxy, CDN, load balancer, identity provider, or browser that + is not in the repository is unknown. If the defect depends on it, the lead + is needs_validation with that behaviour as the blocker. +- Two components reading the same bytes differently is the pattern to hunt: + front end versus back end, validator versus consumer, cache key versus + response. +- Test with two dummy accounts and a local instance only. Never send requests + to a deployed host or a real identity provider. +``` + +## Hunting classes + +### Request framing disagreement + +Where the target parses HTTP or forwards it, compare how each layer decides +where a request ends: conflicting or repeated length headers, chunked encoding +variants, bare line feeds, header folding, upgrade and tunnel handling, +HTTP/2-to-1 downgrades. The defect is a second request hidden from the layer +that applies the security check. Custom parsers and hand-written proxies are +the priority; a framework default is rarely the bug. + +### Cache poisoning through unkeyed input + +A response varies on input the cache key ignores: a forwarded host, a scheme +header, a query parameter stripped from the key, a cookie. Find where the +application reflects or acts on that input, then check what the cache +configuration in the repository keys on. Stored harm to other users is the +result to establish. + +### Cache deception and private responses + +A response containing one user's data is stored and served to another. Look +for path-suffix tricks that make a private route look static, missing or +overridden cache directives on authenticated responses, and normalization +differences between the cache and the router. + +### Host and forwarded-header trust + +Code that builds links, reset URLs, redirects, tenant selection, or +allowlist decisions from `Host` or forwarded headers. Establish which hop is +supposed to set the header and whether the application accepts it from anyone. +Password-reset links built from a caller-supplied host are the classic result. + +### Response header injection + +Caller-controlled text placed in a response header or status line without +removing line breaks: redirect targets, filenames in disposition headers, +cookie values, custom headers echoing input. + +### Cross-site request forgery + +State-changing requests that a third-party page can cause a logged-in browser +to send. Check every method the route accepts, content types that bypass +preflight, token validation that passes when the token is absent, tokens not +tied to the session, and same-site assumptions broken by sibling subdomains. + +### Session fixation and incomplete logout + +The session identifier survives a privilege change, or a session stays valid +after logout, password change, MFA reset, or role removal. Trace issue, +rotation, and invalidation, including long-lived refresh and remember-me +tokens and sessions on other devices. + +### Cookie scope and transport + +Cookies scoped to a parent domain shared with less-trusted sites, session +cookies readable by script or sent over plaintext, and prefix or path rules +that let another application on the domain overwrite them. Only a cookie that +carries authority matters. + +### Token verification and claim binding + +For signed tokens: is the algorithm fixed by the verifier or chosen by the +token, is the key selected by an untrusted header, are issuer, audience, +expiry, and not-before all checked, and is the token accepted by a service it +was not minted for? Follow each claim the code trusts to the decision it +drives. + +### OAuth and OIDC flow binding + +Redirect URI matching that allows open patterns, missing or unbound `state` +and nonce, authorization codes not tied to the client or the PKCE verifier, +tokens from one client accepted by another, and identity taken from an +unverified field of the provider's response. Mix-up between several +configured providers belongs here. + +### SAML assertion binding + +Signature checked on one element while another is consumed, comments or +duplicate nodes changing the parsed identity, assertions without audience, +recipient, or time bounds, and replay of an already used assertion. + +### MFA enrollment and downgrade + +A second factor that can be added, replaced, or removed with only the first +factor; fallback methods weaker than the primary; "remember this device" that +outlives a credential change; endpoints that skip the factor check. + +### Step-up authentication binding + +A recent-authentication or elevated state that is not tied to the specific +action, session, or time window, so it can be obtained for a harmless action +and spent on a sensitive one. + +### Passkey and WebAuthn verification + +Server-side checks on origin, relying-party ID, challenge freshness and +single use, user-presence and user-verification flags, signature counter, and +that the credential belongs to the account being signed in. + +### Account linking and identity collision + +Accounts merged or matched on an unverified email, case or Unicode variants of +the same identifier, a social login attached to an existing account without +proof of control, and pre-registration takeover. + +### Password reset and account recovery + +Tokens that are guessable, long-lived, reusable, not invalidated by a newer +request or a password change, or sent to an address the requester can +influence. Recovery through support flows, security questions, and backup +codes is part of the same boundary. + +### API key scope and exposure + +Keys that carry more reach than the integration needs, are not bound to a +tenant or resource, appear in URLs, logs, or client bundles, or remain valid +after the owner loses access. + +### Mutual TLS identity mapping + +A valid client certificate treated as authorization: any certificate from the +trusted CA accepted for any role, identity taken from a field the client +controls, a forwarded-certificate header trusted from the wrong hop, and +revocation or expiry failures that fall back to allow. + +## Cross-cutting moves + +- For every credential, write the four bindings — user, client, request, + recipient — and find the one the code does not enforce. +- Repeat each check on the legacy route, the mobile API, the admin API, and + the internal endpoint. +- Change one thing at a time: method, content type, parameter location, + duplicate parameter, encoding, case. +- Look at failure handling: verification error, provider timeout, missing + claim, expired key. Fail-open is the finding. +- Compare what is checked at sign-in with what is checked at refresh, reset, + and link. + +## Evidence bar + +- Confirmed needs a local reproduction between two dummy principals or a + dummy client and server, with the request and the wrong outcome recorded. +- A defect that needs a specific proxy, cache, CDN, browser, or identity + provider behaviour is `needs_validation` unless that component's + configuration is in the repository and was exercised locally. +- Missing security headers, cookie flags, or rate limits are hardening notes + unless a traced attack depends on the gap. +- Severity follows what was demonstrated: account takeover, cross-user data, + or a forced action — not the name of the bug class. diff --git a/engineering/security-audit/scripts/audit_common.py b/engineering/security-audit/scripts/audit_common.py new file mode 100644 index 00000000..49d9e931 --- /dev/null +++ b/engineering/security-audit/scripts/audit_common.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +"""Shared contracts for the security-audit tools. + +Holds the vocabularies (unit statuses, verdicts, breadth tiers, run profiles), +the identity rules (agent IDs, fingerprints, coverage IDs), and the input +guards every other script in this folder relies on. Run it directly to print +the contracts an orchestrating agent must honour. + +Usage: + python3 audit_common.py + python3 audit_common.py --format json +""" + +import argparse +import hashlib +import json +import re +import sys +import unicodedata +from pathlib import Path +from typing import Any, Dict, List, Optional, Set, Tuple + +MAX_INPUT_BYTES = 5 * 1024 * 1024 +MAX_ERRORS = 100 + +SEVERITIES = ["informational", "low", "medium", "high", "critical"] +VERDICTS = ["confirmed", "needs_validation", "rejected"] +UNIT_STATUSES = ["planned", "in_progress", "covered", "candidate", "blocked", + "deferred", "out_of_scope", "not_applicable"] +ORIGINS = ["none", "new", "carried_confirmed", "revalidate_confirmed", + "carried_needs_validation", "reopened_gap", "recheck_covered", + "critic"] +PROFILES = ["quick", "standard", "deep"] +REF_KEYS = ["surface", "boundary", "subsystem", "attack_class"] + +CORE_FILE = "attack-classes.md" +FOCUSED_COMPANIONS = ["web-protocol-and-auth.md", "ai-and-llm.md", + "supply-chain-and-release.md", "cloud-and-deployment.md"] +ALL_COMPANIONS = FOCUSED_COMPANIONS + [ + "client-side.md", "data-isolation-and-lifecycle.md", + "desktop-mobile-and-local-ipc.md", "memory-safety-and-binary.md", + "protocols-rpc-and-messaging.md", "resource-exhaustion-and-availability.md"] +TIERS: Dict[str, List[str]] = {"core": [], "focused": FOCUSED_COMPANIONS, + "full": ALL_COMPANIONS} + +AGENT_ID_RE = re.compile(r"^[a-z0-9][a-z0-9_-]{0,63}$") +FINGERPRINT_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._:/@+-]{0,199}$") +BLOCK_RE = re.compile(r"^([a-z0-9-]+\.md)#(\S.*)$") +RESERVED_IDS = {"con", "prn", "aux", "nul"} | { + f"{stem}{n}" for stem in ("com", "lpt") for n in range(1, 10)} + + +class AuditInputError(Exception): + """Raised when an input file cannot be read as the expected document.""" + + +def load_json(path: Path) -> Any: + """Read a JSON document, refusing oversized or malformed input.""" + try: + size = path.stat().st_size + except OSError as exc: + raise AuditInputError(f"cannot read {path}: {exc.strerror}") from exc + if size > MAX_INPUT_BYTES: + raise AuditInputError(f"{path} is {size} bytes; limit is {MAX_INPUT_BYTES}") + try: + return json.loads(path.read_text(encoding="utf-8")) + except json.JSONDecodeError as exc: + raise AuditInputError( + f"{path} is not valid JSON (line {exc.lineno}): {exc.msg}") from exc + except (OSError, UnicodeDecodeError, RecursionError) as exc: + raise AuditInputError(f"{path} is not readable JSON: {exc}") from exc + + +def is_text(value: Any) -> bool: + """True for a string with visible content and no control characters.""" + if not isinstance(value, str) or not value.strip(): + return False + return not any(unicodedata.category(ch) in ("Cc", "Cf", "Cs", "Zl", "Zp") + and ch not in "\n\t" for ch in value) + + +def is_repo_path(value: Any) -> bool: + """True for a relative POSIX path that cannot leave the repository.""" + if not is_text(value) or "\n" in value or "\\" in value: + return False + if value.startswith(("/", "~")) or re.match(r"^[A-Za-z]:", value): + return False + parts = value.split("/") + return all(part not in ("", ".", "..") for part in parts) + + +def is_agent_id(value: Any) -> bool: + """True for a lowercase agent ID that is safe as a directory name.""" + return (isinstance(value, str) and bool(AGENT_ID_RE.match(value)) + and value not in RESERVED_IDS) + + +def is_fingerprint(value: Any) -> bool: + """True for a stable root-cause fingerprint.""" + return isinstance(value, str) and bool(FINGERPRINT_RE.match(value)) + + +def coverage_id(refs: Dict[str, Any]) -> str: + """Derive the deterministic ID of a coverage unit from its references. + + The ID depends only on the source-derived references, so the same unit + gets the same ID in every run regardless of wave, owner, or outcome. + """ + parts = [unicodedata.normalize("NFC", str(refs.get(key, ""))) for key in REF_KEYS] + parts.append(unicodedata.normalize("NFC", str(refs.get("lifecycle", "")))) + digest = hashlib.sha256("\x1f".join(parts).encode("utf-8")).hexdigest() + return f"cu-{digest[:20]}" + + +def parse_block(ref: Any) -> Optional[Tuple[str, str]]: + """Split a `file.md#Heading` block reference into its two halves.""" + if not isinstance(ref, str): + return None + match = BLOCK_RE.match(ref) + return (match.group(1), match.group(2).strip()) if match else None + + +def references_dir() -> Path: + """Location of the skill's reference files relative to this script.""" + return Path(__file__).resolve().parent.parent / "references" + + +def load_block_index(directory: Path) -> Dict[str, Set[str]]: + """Map each reference file to the set of headings usable as blocks.""" + index: Dict[str, Set[str]] = {} + if not directory.is_dir(): + return index + for path in sorted(directory.glob("*.md")): + headings: Set[str] = set() + fenced = False + for line in path.read_text(encoding="utf-8").splitlines(): + if line.startswith("```"): + fenced = not fenced + elif not fenced and re.match(r"^#{2,3} \S", line): + headings.add(line.lstrip("#").strip()) + index[path.name] = headings + return index + + +def emit(report: Dict[str, Any], fmt: str) -> None: + """Print a validation report as JSON or as a short readable summary.""" + if fmt == "json": + print(json.dumps(report, indent=2, sort_keys=True)) + return + verdict = "VALID" if report.get("valid") else "INVALID" + print(f"{verdict}: {report.get('file', '')}") + for key, value in sorted((report.get("stats") or {}).items()): + print(f" {key}: {value}") + errors = report.get("errors") or [] + for message in errors[:MAX_ERRORS]: + print(f" - {message}") + if len(errors) > MAX_ERRORS: + print(f" ... {len(errors) - MAX_ERRORS} more error(s) not shown") + + +def contracts() -> Dict[str, Any]: + """The vocabularies and identity rules shared by every audit tool.""" + return { + "verdicts": VERDICTS, + "severities": SEVERITIES, + "unit_statuses": UNIT_STATUSES, + "unit_origins": ORIGINS, + "profiles": PROFILES, + "tiers": {name: [CORE_FILE] + files for name, files in TIERS.items()}, + "agent_id_pattern": AGENT_ID_RE.pattern, + "fingerprint_pattern": FINGERPRINT_RE.pattern, + "coverage_id": "cu- + first 20 hex of sha256 over surface, boundary, " + "subsystem, attack_class, lifecycle joined by U+001F", + "max_input_bytes": MAX_INPUT_BYTES, + } + + +def main() -> None: + """Print the shared contracts.""" + parser = argparse.ArgumentParser( + description="Print the vocabularies and identity rules shared by the " + "security-audit tools.") + parser.add_argument("--format", choices=["text", "json"], default="text", + help="output format (default: text)") + args = parser.parse_args() + data = contracts() + if args.format == "json": + print(json.dumps(data, indent=2)) + return + for key, value in data.items(): + if isinstance(value, dict): + print(f"{key}:") + for name, files in value.items(): + print(f" {name}: {', '.join(files)}") + elif isinstance(value, list): + print(f"{key}: {', '.join(value)}") + else: + print(f"{key}: {value}") + + +if __name__ == "__main__": + try: + main() + except BrokenPipeError: + sys.exit(0) diff --git a/engineering/security-audit/scripts/audit_run.py b/engineering/security-audit/scripts/audit_run.py new file mode 100644 index 00000000..a2a2a5cf --- /dev/null +++ b/engineering/security-audit/scripts/audit_run.py @@ -0,0 +1,264 @@ +#!/usr/bin/env python3 +"""Set up and summarise a security audit run. + +Subcommands: + init create the run directory, run-metadata.json, and empty + ledger and findings files outside the target repository + coverage-id derive the deterministic ID of a coverage unit + budget split an agent budget into recon, hunters, critics, verifiers + summary count ledger and findings state for the final report + +Exit codes: 0 success, 1 the request cannot be satisfied (budget too small, +unsafe output location), 2 unreadable input or usage error. + +Usage: + python3 audit_run.py init --target ~/code/app --tier focused + python3 audit_run.py coverage-id --surface "src/api.py#POST /orders" \ + --boundary "src/authz.py#require_owner" --subsystem services/orders \ + --attack-class "attack-classes.md#Access control" + python3 audit_run.py budget --budget 40 --profile standard --units 28 + python3 audit_run.py summary --run-dir ~/security-audits/app/run-1 +""" + +import argparse +import json +import math +import re +import subprocess +import sys +from datetime import datetime, timezone +from pathlib import Path +from typing import Any, Dict, List, Optional + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from audit_common import ( # noqa: E402 + PROFILES, SEVERITIES, TIERS, AuditInputError, coverage_id, is_repo_path, + load_json) + +RECON_AGENTS = 4 +CRITICS = {"quick": 1, "standard": 2, "deep": 2} +VERIFIERS_PER_CANDIDATE = {"quick": 1, "standard": 2, "deep": 2} + + +def git(target: Path, *args: str) -> Optional[str]: + """Run a read-only git query in the target; None when unavailable. + + The target is untrusted: its own git config must not run a helper, and the + query must not rewrite its index. + """ + try: + done = subprocess.run(["git", "-c", "core.fsmonitor=false", + "-c", "safe.bareRepository=explicit", + "--no-optional-locks", "-C", str(target), *args], + check=False, + capture_output=True, text=True, timeout=20) + except (OSError, subprocess.SubprocessError): + return None + return done.stdout.strip() if done.returncode == 0 else None + + +def plan_budget(budget: int, profile: str, units: int, + candidates: Optional[int]) -> Dict[str, Any]: + """Reserve recon, critics, and verifiers first; hunters get the rest.""" + critics = CRITICS[profile] + per_candidate = VERIFIERS_PER_CANDIDATE[profile] + minimum = RECON_AGENTS + critics + per_candidate + remaining = budget - RECON_AGENTS - critics + if candidates is None: + reserve = max(per_candidate, math.ceil(max(remaining, 0) * 0.3)) + else: + reserve = max(per_candidate, candidates * per_candidate) + hunters = max(0, remaining - reserve) + return { + "budget": budget, "profile": profile, "minimum_budget": minimum, + "feasible": budget >= minimum, "recon": RECON_AGENTS, "critics": critics, + "validation_reserve": reserve, "hunters": hunters, "units": units, + "units_deferred": max(0, units - hunters), + "defer_reason": "budget_cannot_fund_hunters" if units > hunters else None, + } + + +def cmd_init(args: argparse.Namespace) -> Dict[str, Any]: + """Create run-N for the target and write its starting files.""" + target = args.target.expanduser().resolve() + if not target.is_dir(): + raise AuditInputError(f"target is not a directory: {target}") + for scope in args.scope: + if not is_repo_path(scope): + raise AuditInputError(f"--scope {scope!r} must be repository-relative") + repo = re.sub(r"[^A-Za-z0-9._-]", "-", target.name) or "target" + root = (args.output_root.expanduser().resolve()) / repo + if (root == target or target in root.parents) and not args.allow_inside_target: + return {"ok": False, "error": "output directory is inside the target; " + "choose a path outside it, or pass --allow-inside-target only " + "when version control ignores that whole directory"} + if args.budget is not None: + gate = plan_budget(args.budget, args.profile, 0, None) + if not gate["feasible"]: + return {"ok": False, "error": f"budget {args.budget} cannot fund " + f"reconnaissance, critics, and one verifier (minimum " + f"{gate['minimum_budget']}); raise it, narrow the scope, " + "or pick the quick profile"} + number = 1 + while (root / f"run-{number}").exists(): + number += 1 + prior = sorted(str(path.parent) for path in root.glob("run-*/coverage-ledger.json")) + run_dir = root / f"run-{number}" + (run_dir / "agents").mkdir(parents=True) + run_id = f"{repo}-run-{number}" + commit = git(target, "rev-parse", "HEAD") + status = git(target, "status", "--porcelain") + metadata = { + "schema_version": 1, "run_id": run_id, "repo": repo, "target": str(target), + "source_ref": {"commit": commit, + "dirty": None if status is None else bool(status)}, + "tier": args.tier, "companions": TIERS[args.tier], "profile": args.profile, + "scope_paths": args.scope, "budget": args.budget, "agents_spent": 0, + "execution_policy": "source-review-and-sandboxed-local-only", + "prior_runs": prior, "shared_file_owner": "lead", + "run_status": "in_progress", "incomplete_reason": None, + "created": datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%SZ"), + } + files = {"run-metadata.json": metadata, + "coverage-ledger.json": {"schema_version": 1, "run_id": run_id, "units": []}, + "findings.json": {"schema_version": 1, "run_id": run_id, "findings": []}} + for name, body in files.items(): + (run_dir / name).write_text(json.dumps(body, indent=2) + "\n", encoding="utf-8") + return {"ok": True, "run_dir": str(run_dir), "run_id": run_id, + "tier": args.tier, "companions": TIERS[args.tier], + "profile": args.profile, "prior_runs": prior, + "partial_coverage": args.profile == "quick" or bool(args.scope) + or args.tier != "full"} + + +def cmd_summary(args: argparse.Namespace) -> Dict[str, Any]: + """Count unit states and verdicts, and list every disclosed gap.""" + run_dir = args.run_dir.expanduser() + metadata = load_json(run_dir / "run-metadata.json") + units = load_json(run_dir / "coverage-ledger.json").get("units", []) + records = load_json(run_dir / "findings.json").get("findings", []) + statuses: Dict[str, int] = {} + gaps: List[Dict[str, Any]] = [] + linked: set = set() + for unit in units: + status = unit.get("status", "unknown") + statuses[status] = statuses.get(status, 0) + 1 + if status == "candidate": + linked |= set(unit.get("fingerprints", [])) + if status in ("deferred", "out_of_scope", "blocked", "planned", "in_progress"): + gaps.append({"coverage_id": unit.get("coverage_id"), "status": status, + "label": unit.get("label"), + "reason": "; ".join(unit.get("unresolved", []))}) + verdicts: Dict[str, int] = {} + severities = {level: 0 for level in reversed(SEVERITIES)} + for record in records: + verdict = record.get("verdict", "unknown") + verdicts[verdict] = verdicts.get(verdict, 0) + 1 + if verdict == "confirmed": + severities[record["severity"]["overall"]] += 1 + recorded = {record.get("fingerprint") for record in records} + return { + "ok": True, "run_id": metadata.get("run_id"), "tier": metadata.get("tier"), + "profile": metadata.get("profile"), "scope_paths": metadata.get("scope_paths"), + "run_status": metadata.get("run_status"), + "budget": metadata.get("budget"), "agents_spent": metadata.get("agents_spent"), + "units": statuses, "verdicts": verdicts, "confirmed_by_severity": severities, + "gaps": gaps, "unvalidated_candidates": sorted(linked - recorded), + } + + +def render(command: str, result: Dict[str, Any]) -> str: + """Readable output; the summary form pastes into REPORT.md.""" + if not result.get("ok", True): + return f"ERROR: {result['error']}" + if command == "coverage-id": + return result["coverage_id"] + if command != "summary": + return "\n".join(f"{key}: {value}" for key, value in result.items() + if key != "ok") + lines = [f"Run {result['run_id']} - tier {result['tier']}, profile " + f"{result['profile']}, status {result['run_status']}", "", + "| Unit status | Count |", "|---|---|"] + lines += [f"| {name} | {count} |" for name, count in sorted(result["units"].items())] + lines += ["", "| Verdict | Count |", "|---|---|"] + lines += [f"| {name} | {count} |" for name, count in sorted(result["verdicts"].items())] + lines += ["", "| Confirmed severity | Count |", "|---|---|"] + lines += [f"| {name} | {count} |" + for name, count in result["confirmed_by_severity"].items()] + if result["gaps"]: + lines += ["", "| Coverage gap | Status | Reason |", "|---|---|---|"] + lines += [f"| {gap['label']} | {gap['status']} | {gap['reason'] or '-'} |" + for gap in result["gaps"]] + if result["unvalidated_candidates"]: + lines += ["", "Unvalidated candidates (not findings): " + + ", ".join(result["unvalidated_candidates"])] + return "\n".join(lines) + + +def build_parser() -> argparse.ArgumentParser: + """Define the subcommands and their options.""" + parser = argparse.ArgumentParser( + description="Set up and summarise a security audit run.") + parser.add_argument("--format", choices=["text", "json"], default="text", + help="output format (default: text)") + sub = parser.add_subparsers(dest="command", required=True) + init = sub.add_parser("init", help="create a run directory outside the target") + init.add_argument("--target", type=Path, required=True, help="repository root to audit") + init.add_argument("--output-root", type=Path, default=Path("~/security-audits"), + help="parent of /run- (default: ~/security-audits)") + init.add_argument("--tier", choices=sorted(TIERS), default="focused", + help="breadth: core, focused (core + 4 companions), full (all 10)") + init.add_argument("--profile", choices=PROFILES, default="standard", + help="depth and redundancy of the run (default: standard)") + init.add_argument("--scope", action="append", default=[], metavar="PATH", + help="limit the run to a repository-relative path (repeatable)") + init.add_argument("--budget", type=int, help="maximum agent invocations") + init.add_argument("--allow-inside-target", action="store_true", + help="permit an output root inside the target (must be git-ignored)") + ident = sub.add_parser("coverage-id", help="derive a coverage unit ID") + for name in ("surface", "boundary", "subsystem", "attack-class"): + ident.add_argument(f"--{name}", required=True, help=f"canonical {name} reference") + ident.add_argument("--lifecycle", help="lifecycle mode, when it is material") + budget = sub.add_parser("budget", help="allocate an agent budget") + budget.add_argument("--budget", type=int, required=True, help="maximum agent invocations") + budget.add_argument("--profile", choices=PROFILES, default="standard") + budget.add_argument("--units", type=int, required=True, help="planned coverage units") + budget.add_argument("--candidates", type=int, + help="expected candidates (default: reserve 30%% of the balance)") + summary = sub.add_parser("summary", help="count ledger and findings state") + summary.add_argument("--run-dir", type=Path, required=True, help="run directory") + return parser + + +def main() -> None: + """Dispatch the chosen subcommand and set the exit code.""" + args = build_parser().parse_args() + try: + if args.command == "init": + result = cmd_init(args) + elif args.command == "summary": + result = cmd_summary(args) + elif args.command == "budget": + result = plan_budget(args.budget, args.profile, args.units, args.candidates) + result["ok"] = result["feasible"] + if not result["feasible"]: + result["error"] = (f"budget below the minimum of " + f"{result['minimum_budget']} for this profile") + else: + refs = {"surface": args.surface, "boundary": args.boundary, + "subsystem": args.subsystem, "attack_class": args.attack_class} + if args.lifecycle: + refs["lifecycle"] = args.lifecycle + result = {"ok": True, "coverage_id": coverage_id(refs), "refs": refs} + except (AuditInputError, OSError, KeyError, TypeError, AttributeError) as exc: + print(f"ERROR: {exc}", file=sys.stderr) + sys.exit(2) + if args.format == "json": + print(json.dumps(result, indent=2)) + else: + print(render(args.command, result)) + sys.exit(0 if result.get("ok", True) else 1) + + +if __name__ == "__main__": + main() diff --git a/engineering/security-audit/scripts/ledger_rules.py b/engineering/security-audit/scripts/ledger_rules.py new file mode 100644 index 00000000..d2b57650 --- /dev/null +++ b/engineering/security-audit/scripts/ledger_rules.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +"""Unit-level rules behind the coverage-ledger validator. + +A coverage unit is one (surface, boundary, subsystem, attack class) cell of +the audit plan. This module holds the state table every unit must obey, the +evidence rules for its checks, and the archive rules for reopened attempts. +Run it directly to print the state table. + +Usage: + python3 ledger_rules.py + python3 ledger_rules.py --format json +""" + +import argparse +import json +import sys +from pathlib import Path +from typing import Any, Dict, List, Optional, Set + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from audit_common import ( # noqa: E402 + CORE_FILE, ORIGINS, REF_KEYS, UNIT_STATUSES, coverage_id, is_agent_id, + is_fingerprint, is_repo_path, is_text, parse_block) + +# status -> (owner, reviewed_paths + checks, fingerprints, unresolved) +STATE_TABLE: Dict[str, tuple] = { + "planned": ("none", "empty", "empty", "empty"), + "in_progress": ("set", "empty", "empty", "empty"), + "covered": ("set", "filled", "empty", "empty"), + "candidate": ("set", "filled", "filled", "any"), + "blocked": ("set", "filled", "empty", "filled"), + "deferred": ("none", "empty", "empty", "filled"), + "out_of_scope": ("none", "empty", "empty", "filled"), + "not_applicable": ("none", "empty", "empty", "filled"), +} +ARCHIVABLE = ("covered", "candidate", "blocked") +UNIT_FIELDS = ["coverage_id", "refs", "label", "starting_paths", "class_blocks", + "excluded_blocks", "origin", "wave", "status", "owner", + "reviewed_paths", "checks", "fingerprints", "unresolved", + "attempts"] +ATTEMPT_FIELDS = ["wave", "status", "owner", "reviewed_paths", "checks", + "fingerprints", "unresolved", "reopen_reason"] + + +def _fill(errors: List[str], base: str, name: str, value: Any, rule: str) -> None: + """Apply an empty / filled / any rule to one list field.""" + if not isinstance(value, list): + errors.append(f"{base}.{name}: expected an array") + elif rule == "empty" and value: + errors.append(f"{base}.{name}: must be empty for this status") + elif rule == "filled" and not value: + errors.append(f"{base}.{name}: must not be empty for this status") + + +def check_state(record: Dict[str, Any], base: str) -> List[str]: + """Enforce the state table on a live unit or an archived attempt.""" + errors: List[str] = [] + status = record.get("status") + if not isinstance(status, str) or status not in STATE_TABLE: + return [f"{base}.status: {status!r} is not one of {UNIT_STATUSES}"] + owner_rule, evidence_rule, print_rule, open_rule = STATE_TABLE[status] + owner = record.get("owner") + if owner_rule == "none" and owner is not None: + errors.append(f"{base}.owner: a {status} unit has no owner") + if owner_rule == "set" and not is_agent_id(owner): + errors.append(f"{base}.owner: a {status} unit needs a lowercase agent ID") + _fill(errors, base, "reviewed_paths", record.get("reviewed_paths"), evidence_rule) + _fill(errors, base, "checks", record.get("checks"), evidence_rule) + _fill(errors, base, "fingerprints", record.get("fingerprints"), print_rule) + _fill(errors, base, "unresolved", record.get("unresolved"), open_rule) + return errors + + +def check_evidence(record: Dict[str, Any], base: str, + run_dir: Optional[Path]) -> List[str]: + """Validate checks, their evidence files, and the reviewed-path union.""" + errors: List[str] = [] + owned: Set[str] = set() + checks = record.get("checks") if isinstance(record.get("checks"), list) else [] + for index, item in enumerate(checks): + where = f"{base}.checks[{index}]" + if not isinstance(item, dict): + errors.append(f"{where}: expected an object") + continue + agent = item.get("agent_id") + if not is_agent_id(agent): + errors.append(f"{where}.agent_id: expected a lowercase agent ID") + for name in ("invariant", "result"): + if not is_text(item.get(name)): + errors.append(f"{where}.{name}: must contain visible text") + paths = item.get("reviewed_paths") + if not isinstance(paths, list) or not paths: + errors.append(f"{where}.reviewed_paths: must not be empty") + else: + for path in paths: + if is_repo_path(path): + owned.add(path) + else: + errors.append(f"{where}.reviewed_paths: {path!r} is not a " + "repository-relative path") + method, evidence = item.get("method"), item.get("evidence_file") + if method == "source": + if evidence is not None: + errors.append(f"{where}.evidence_file: a source check uses null") + elif method == "local": + prefix = f"agents/{agent}/evidence/" + if not (is_repo_path(evidence) and str(evidence).startswith(prefix) + and len(str(evidence)) > len(prefix)): + errors.append(f"{where}.evidence_file: a local check needs a " + f"promoted file under {prefix}") + elif run_dir is not None: + target = run_dir / str(evidence) + if target.is_symlink() or not target.is_file(): + errors.append(f"{where}.evidence_file: {evidence} is not a " + "regular file in the run directory") + else: + errors.append(f"{where}.method: expected 'source' or 'local'") + listed = record.get("reviewed_paths") + if isinstance(listed, list): + for path in listed: + if not is_repo_path(path): + errors.append(f"{base}.reviewed_paths: {path!r} is not a " + "repository-relative path") + if set(p for p in listed if isinstance(p, str)) != owned: + errors.append(f"{base}.reviewed_paths: must equal the union of the " + "paths owned by its checks") + for name, test in (("fingerprints", is_fingerprint), ("unresolved", is_text)): + values = record.get(name) if isinstance(record.get(name), list) else [] + for value in values: + if not test(value): + errors.append(f"{base}.{name}: invalid entry {value!r}") + if len(set(map(str, values))) != len(values): + errors.append(f"{base}.{name}: entries must be unique") + return errors + + +def check_blocks(unit: Dict[str, Any], base: str, ctx: Dict[str, Any]) -> List[str]: + """Validate the class blocks a hunter is given and those ruled out.""" + errors: List[str] = [] + index: Optional[Dict[str, Set[str]]] = ctx.get("block_index") + allowed: Optional[List[str]] = ctx.get("companions") + selected = unit.get("class_blocks") + if not isinstance(selected, list) or not selected: + return [f"{base}.class_blocks: list at least the primary attack-class block"] + refs = unit.get("refs") if isinstance(unit.get("refs"), dict) else {} + if refs.get("attack_class") not in selected: + errors.append(f"{base}.class_blocks: must include refs.attack_class") + excluded = unit.get("excluded_blocks") + excluded = excluded if isinstance(excluded, list) else [] + ruled_out = [] + for position, entry in enumerate(excluded): + if not isinstance(entry, dict) or not is_text(entry.get("reason")): + errors.append(f"{base}.excluded_blocks[{position}]: needs a block " + "and a reason") + continue + ruled_out.append(entry.get("block")) + for block in ruled_out: + if block in selected: + errors.append(f"{base}.excluded_blocks: {block!r} is also selected") + if len(set(map(str, selected))) != len(selected): + errors.append(f"{base}.class_blocks: entries must be unique") + for block in list(selected) + ruled_out: + parsed = parse_block(block) + if not parsed: + errors.append(f"{base}: {block!r} is not a 'file.md#Heading' block") + continue + name, heading = parsed + if index is not None and heading not in index.get(name, set()): + errors.append(f"{base}: block {block!r} does not match a heading " + f"in references/{name}") + if allowed is not None and block in selected \ + and name != CORE_FILE and name not in allowed: + errors.append(f"{base}.class_blocks: {name} is outside the " + "companions selected for this run") + return errors + + +def check_attempts(unit: Dict[str, Any], base: str, + run_dir: Optional[Path]) -> List[str]: + """Archived attempts keep their own owner and evidence, in wave order.""" + errors: List[str] = [] + attempts = unit.get("attempts") + if not isinstance(attempts, list): + return [f"{base}.attempts: expected an array"] + live_wave = unit.get("wave") if isinstance(unit.get("wave"), int) else 0 + owners: Set[str] = set() + files: Set[str] = set() + last_wave = 0 + for position, attempt in enumerate(attempts): + where = f"{base}.attempts[{position}]" + if not isinstance(attempt, dict): + errors.append(f"{where}: expected an object") + continue + missing = [name for name in ATTEMPT_FIELDS if name not in attempt] + if missing: + errors.append(f"{where}: missing {missing}") + continue + if not isinstance(attempt["checks"], list) \ + or not isinstance(attempt["owner"], (str, type(None))): + errors.append(f"{where}: 'checks' must be an array and 'owner' a string") + continue + if attempt["status"] not in ARCHIVABLE: + errors.append(f"{where}.status: only {list(ARCHIVABLE)} can be archived") + continue + errors += check_state(attempt, where) + check_evidence(attempt, where, run_dir) + wave = attempt["wave"] + if not isinstance(wave, int) or isinstance(wave, bool) \ + or wave <= last_wave or wave >= live_wave: + errors.append(f"{where}.wave: waves increase and stay below the " + f"live wave {live_wave}") + else: + last_wave = wave + if not is_text(attempt["reopen_reason"]): + errors.append(f"{where}.reopen_reason: say why the unit was reopened") + if attempt["owner"] in owners: + errors.append(f"{where}.owner: each attempt needs a fresh owner") + owners.add(str(attempt["owner"])) + files |= {str(c.get("evidence_file")) for c in attempt["checks"] + if isinstance(c, dict) and c.get("evidence_file")} + if isinstance(unit.get("owner"), str) and unit.get("owner") in owners: + errors.append(f"{base}.owner: a reopened unit needs a fresh owner") + live = unit.get("checks") if isinstance(unit.get("checks"), list) else [] + for item in live: + if isinstance(item, dict) and (str(item.get("agent_id")) in owners + or str(item.get("evidence_file")) in files): + errors.append(f"{base}.checks: evidence from an archived attempt " + "cannot be reused in the live state") + return errors + + +def check_unit(unit: Any, index: int, ctx: Dict[str, Any]) -> List[str]: + """Run every rule against one coverage unit.""" + base = f"units[{index}]" + if not isinstance(unit, dict): + return [f"{base}: expected an object"] + missing = [name for name in UNIT_FIELDS if name not in unit] + if missing: + return [f"{base}: missing {missing}"] + errors: List[str] = [] + refs = unit["refs"] + if not isinstance(refs, dict) or set(refs) - set(REF_KEYS + ["lifecycle"]) \ + or any(not is_text(refs.get(key)) for key in REF_KEYS) \ + or ("lifecycle" in refs and not is_text(refs["lifecycle"])): + errors.append(f"{base}.refs: needs text for {REF_KEYS} and, optionally, " + "lifecycle; no other keys") + elif unit["coverage_id"] != coverage_id(refs): + errors.append(f"{base}.coverage_id: expected {coverage_id(refs)} for " + "these refs") + if not is_text(unit["label"]): + errors.append(f"{base}.label: must contain visible text") + paths = unit["starting_paths"] + if not isinstance(paths, list) or not paths \ + or any(not is_repo_path(path) for path in paths): + errors.append(f"{base}.starting_paths: list repository-relative paths") + if unit["origin"] not in ORIGINS: + errors.append(f"{base}.origin: {unit['origin']!r} is not one of {ORIGINS}") + wave = unit["wave"] + if not isinstance(wave, int) or isinstance(wave, bool) or wave < 1: + errors.append(f"{base}.wave: expected a positive integer") + run_dir = ctx.get("run_dir") + errors += check_state(unit, base) + check_evidence(unit, base, run_dir) + errors += check_blocks(unit, base, ctx) + check_attempts(unit, base, run_dir) + return errors + + +def main() -> None: + """Print the unit state table.""" + parser = argparse.ArgumentParser( + description="Print the state table every coverage unit must obey.") + parser.add_argument("--format", choices=["text", "json"], default="text", + help="output format (default: text)") + args = parser.parse_args() + columns = ["owner", "reviewed_paths_and_checks", "fingerprints", "unresolved"] + if args.format == "json": + print(json.dumps({status: dict(zip(columns, rule)) + for status, rule in STATE_TABLE.items()}, indent=2)) + return + print(f"{'status':<16}" + "".join(f"{name:<28}" for name in columns)) + for status, rule in STATE_TABLE.items(): + print(f"{status:<16}" + "".join(f"{cell:<28}" for cell in rule)) + + +if __name__ == "__main__": + main() diff --git a/engineering/security-audit/scripts/promote_evidence.py b/engineering/security-audit/scripts/promote_evidence.py new file mode 100644 index 00000000..7fcd1546 --- /dev/null +++ b/engineering/security-audit/scripts/promote_evidence.py @@ -0,0 +1,212 @@ +#!/usr/bin/env python3 +"""Promote sandbox output into an agent's retained evidence directory. + +Files a hunter or verifier produces inside agents//scratch/ stay +target-controlled even after the sandbox exits: a build or test can leave +symlinks, hard links, FIFOs, or huge files behind. This tool is the only path +from scratch/ to evidence/. It copies named files one at a time through +directory descriptors that never follow links, accepts only single-link +regular files within the byte limits, re-checks the file after reading, and +creates each destination exclusively. + +Run it only after the sandbox and every process it started have exited. A +rejected file is not evidence: keep the lead as needs_validation and record +the rejection as its blocker. + +Exit codes: 0 every file promoted, 1 at least one file rejected, 2 usage +error or unsupported platform. + +Usage: + python3 promote_evidence.py --run-dir ~/security-audits/app/run-1 \ + --agent-id hunter-03 --file out/result.txt --file out/trace.log + python3 promote_evidence.py --run-dir . --agent-id verifier-07 \ + --file result.json --max-bytes 65536 --format json +""" + +import argparse +import hashlib +import json +import os +import stat +import sys +from pathlib import Path +from typing import Any, Dict, List + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from audit_common import is_agent_id, is_repo_path # noqa: E402 + +DIR_FLAGS = os.O_RDONLY | getattr(os, "O_DIRECTORY", 0) | getattr(os, "O_NOFOLLOW", 0) +CHUNK = 64 * 1024 + + +class Rejected(Exception): + """Raised when a file fails a promotion check.""" + + +def supported() -> bool: + """Descriptor-relative, no-follow file access is required.""" + return (hasattr(os, "O_NOFOLLOW") and os.open in os.supports_dir_fd + and os.mkdir in os.supports_dir_fd) + + +def open_dir(parent_fd: int, name: str, create: bool) -> int: + """Open one directory component without following a link.""" + if create: + try: + os.mkdir(name, 0o700, dir_fd=parent_fd) + except FileExistsError: + pass + try: + return os.open(name, DIR_FLAGS, dir_fd=parent_fd) + except OSError as exc: + raise Rejected(f"directory component {name!r} is not a real directory " + f"({exc.strerror})") from exc + + +def walk(root_fd: int, parts: List[str], create: bool) -> int: + """Descend through directory components, returning the last descriptor.""" + current = os.dup(root_fd) + for name in parts: + try: + following = open_dir(current, name, create) + finally: + os.close(current) + current = following + return current + + +def read_checked(parent_fd: int, name: str, limit: int) -> bytes: + """Read a single-link regular file, rejecting anything that changes.""" + flags = os.O_RDONLY | os.O_NOFOLLOW | getattr(os, "O_NONBLOCK", 0) + try: + fd = os.open(name, flags, dir_fd=parent_fd) + except OSError as exc: + raise Rejected(f"cannot open without following links ({exc.strerror})") from exc + try: + before = os.fstat(fd) + if not stat.S_ISREG(before.st_mode): + raise Rejected("not a regular file") + if before.st_nlink != 1: + raise Rejected("file has more than one hard link") + if before.st_size > limit: + raise Rejected(f"{before.st_size} bytes exceeds the limit of {limit}") + data = bytearray() + while len(data) <= limit: + chunk = os.read(fd, CHUNK) + if not chunk: + break + data += chunk + after = os.fstat(fd) + same = (before.st_ino, before.st_dev, before.st_size, before.st_mtime_ns, + before.st_nlink) == (after.st_ino, after.st_dev, after.st_size, + after.st_mtime_ns, after.st_nlink) + if len(data) != before.st_size or not same: + raise Rejected("file changed while it was being read") + return bytes(data) + finally: + os.close(fd) + + +def write_exclusive(parent_fd: int, name: str, data: bytes) -> None: + """Create the destination file; refuse to reuse an existing name.""" + flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW + try: + fd = os.open(name, flags, 0o600, dir_fd=parent_fd) + except OSError as exc: + raise Rejected(f"destination cannot be created exclusively ({exc.strerror})") from exc + try: + info = os.fstat(fd) + if not stat.S_ISREG(info.st_mode) or info.st_nlink != 1: + raise Rejected("destination is not a fresh regular file") + view = memoryview(data) + while view: + written = os.write(fd, view) + view = view[written:] + finally: + os.close(fd) + + +def promote(agent_dir: Path, relative: str, limit: int) -> Dict[str, Any]: + """Copy one scratch file into evidence/ under the same relative path.""" + parts = relative.split("/") + root_fd = os.open(str(agent_dir), DIR_FLAGS) + scratch_fd = evidence_fd = source_dir = dest_dir = -1 + try: + scratch_fd = open_dir(root_fd, "scratch", create=False) + evidence_fd = open_dir(root_fd, "evidence", create=True) + source_dir = walk(scratch_fd, parts[:-1], create=False) + data = read_checked(source_dir, parts[-1], limit) + dest_dir = walk(evidence_fd, parts[:-1], create=True) + write_exclusive(dest_dir, parts[-1], data) + finally: + for fd in (source_dir, dest_dir, scratch_fd, evidence_fd, root_fd): + if fd >= 0: + os.close(fd) + return {"file": relative, "promoted": True, "bytes": len(data), + "sha256": hashlib.sha256(data).hexdigest(), + "evidence_file": f"agents/{agent_dir.name}/evidence/{relative}"} + + +def run(args: argparse.Namespace) -> Dict[str, Any]: + """Promote each requested file inside the per-file and total limits.""" + agent_dir = args.run_dir.expanduser().resolve() / "agents" / args.agent_id + results: List[Dict[str, Any]] = [] + total = 0 + for relative in args.file: + try: + if not is_repo_path(relative): + raise Rejected("path must be relative, with no '..', '.', or " + "empty components") + remaining = args.max_total_bytes - total + if remaining <= 0: + raise Rejected("cumulative byte limit already reached") + outcome = promote(agent_dir, relative, min(args.max_bytes, remaining)) + total += outcome["bytes"] + except (Rejected, OSError) as exc: + outcome = {"file": relative, "promoted": False, "reason": str(exc)} + results.append(outcome) + return {"agent_id": args.agent_id, "total_bytes": total, "results": results, + "all_promoted": all(item["promoted"] for item in results)} + + +def main() -> None: + """Parse arguments, promote the files, and set the exit code.""" + parser = argparse.ArgumentParser( + description="Safely copy named files from an agent's scratch/ to its " + "evidence/ directory.") + parser.add_argument("--run-dir", type=Path, required=True, help="audit run directory") + parser.add_argument("--agent-id", required=True, help="owner of the scratch directory") + parser.add_argument("--file", action="append", required=True, metavar="REL", + help="scratch-relative file to promote (repeatable); " + "name files explicitly, never a directory or glob") + parser.add_argument("--max-bytes", type=int, default=1024 * 1024, + help="per-file byte limit (default: 1 MiB)") + parser.add_argument("--max-total-bytes", type=int, default=8 * 1024 * 1024, + help="cumulative byte limit for this call (default: 8 MiB)") + parser.add_argument("--format", choices=["text", "json"], default="text", + help="output format (default: text)") + args = parser.parse_args() + if not is_agent_id(args.agent_id): + print("ERROR: --agent-id must match ^[a-z0-9][a-z0-9_-]{0,63}$", + file=sys.stderr) + sys.exit(2) + if not supported(): + print("ERROR: this platform lacks no-follow descriptor-relative file " + "access; treat sandbox output as unavailable evidence", + file=sys.stderr) + sys.exit(2) + report = run(args) + if args.format == "json": + print(json.dumps(report, indent=2)) + else: + for item in report["results"]: + if item["promoted"]: + print(f"PROMOTED {item['evidence_file']} " + f"({item['bytes']} bytes, sha256 {item['sha256'][:16]})") + else: + print(f"REJECTED {item['file']}: {item['reason']}") + sys.exit(0 if report["all_promoted"] else 1) + + +if __name__ == "__main__": + main() diff --git a/engineering/security-audit/scripts/validate_coverage_ledger.py b/engineering/security-audit/scripts/validate_coverage_ledger.py new file mode 100644 index 00000000..b388b545 --- /dev/null +++ b/engineering/security-audit/scripts/validate_coverage_ledger.py @@ -0,0 +1,188 @@ +#!/usr/bin/env python3 +"""Validate an audit run's coverage-ledger.json. + +The ledger is the audit's coverage claim, so it is checked after seeding and +after every update: deterministic unit IDs, sort order, the per-status state +table, owned evidence, resolvable attack-class blocks, and reopened-attempt +archives. Optional inputs tighten the check: run metadata confines companion +blocks to the selected breadth tier, findings are cross-linked by fingerprint, +and --final refuses a ledger that still has unassigned or running work. + +Exit codes: 0 valid, 1 invalid, 2 unreadable input or usage error. + +Usage: + python3 validate_coverage_ledger.py coverage-ledger.json + python3 validate_coverage_ledger.py coverage-ledger.json \ + --metadata run-metadata.json --findings findings.json \ + --run-dir . --final --format json +""" + +import argparse +import sys +from pathlib import Path +from typing import Any, Dict, List, Optional, Set + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from audit_common import ( # noqa: E402 + AuditInputError, emit, is_text, load_block_index, load_json, references_dir) +from ledger_rules import check_unit # noqa: E402 + +MAX_UNITS = 5000 + + +def check_order(units: List[Any], errors: List[str]) -> None: + """Units are sorted by coverage ID and each ID appears once.""" + seen: Dict[str, int] = {} + previous: Optional[str] = None + for index, unit in enumerate(units): + identifier = unit.get("coverage_id") if isinstance(unit, dict) else None + if not isinstance(identifier, str): + continue + if identifier in seen: + errors.append(f"units[{index}].coverage_id: duplicates units" + f"[{seen[identifier]}]; the same refs describe one unit") + seen.setdefault(identifier, index) + if previous is not None and identifier < previous: + errors.append(f"units[{index}].coverage_id: units must be sorted " + "by coverage_id") + previous = identifier + + +def live_fingerprints(units: List[Any], status: Optional[str] = None) -> Set[str]: + """Fingerprints linked from live unit state, optionally for one status.""" + found: Set[str] = set() + for unit in units: + if not isinstance(unit, dict): + continue + if status is not None and unit.get("status") != status: + continue + values = unit.get("fingerprints") + if isinstance(values, list): + found |= {value for value in values if isinstance(value, str)} + return found + + +def check_findings(units: List[Any], findings_doc: Any, final: bool, + incomplete: bool, errors: List[str], + stats: Dict[str, Any]) -> None: + """Every record links to a unit, and every candidate reaches a verdict.""" + records = findings_doc.get("findings") if isinstance(findings_doc, dict) else None + if not isinstance(records, list): + errors.append("findings: expected an object with a 'findings' array") + return + recorded = {record.get("fingerprint") for record in records + if isinstance(record, dict)} + linked = live_fingerprints(units) + for fingerprint in sorted(str(value) for value in recorded - linked): + errors.append(f"findings: {fingerprint!r} is not linked from any " + "coverage unit") + pending = sorted(live_fingerprints(units, "candidate") - recorded) + stats["unvalidated_candidates"] = len(pending) + if final and pending and not incomplete: + for fingerprint in pending: + errors.append(f"ledger: candidate {fingerprint!r} has no final " + "record; validate it or mark the run incomplete") + + +def check_run_ids(ledger: Dict[str, Any], metadata: Any, findings_doc: Any, + errors: List[str]) -> None: + """All shared files of a run carry the same run ID.""" + run_id = ledger.get("run_id") + for name, document in (("metadata", metadata), ("findings", findings_doc)): + if isinstance(document, dict) and document.get("run_id") != run_id: + errors.append(f"{name}.run_id: {document.get('run_id')!r} does not " + f"match the ledger run_id {run_id!r}") + + +def validate(ledger: Any, ctx: Dict[str, Any], metadata: Any = None, + findings_doc: Any = None, final: bool = False) -> Dict[str, Any]: + """Run every ledger rule and return a report with status counts.""" + errors: List[str] = [] + stats: Dict[str, Any] = {} + if not isinstance(ledger, dict) or not isinstance(ledger.get("units"), list): + return {"valid": False, "error_count": 1, "stats": stats, "errors": [ + "$: expected an object with schema_version, run_id, and a 'units' array"]} + if ledger.get("schema_version") != 1: + errors.append("$.schema_version: must equal 1") + if not is_text(ledger.get("run_id")): + errors.append("$.run_id: must contain visible text") + units = ledger["units"] + if len(units) > MAX_UNITS: + errors.append(f"$.units: {len(units)} units exceeds the {MAX_UNITS} limit; " + "split the audit into scoped runs") + units = units[:MAX_UNITS] + for index, unit in enumerate(units): + errors += check_unit(unit, index, ctx) + check_order(units, errors) + for unit in units: + status = unit.get("status") if isinstance(unit, dict) else None + if isinstance(status, str): + stats[status] = stats.get(status, 0) + 1 + stats["total"] = len(units) + incomplete = isinstance(metadata, dict) and metadata.get("run_status") == "incomplete" + check_run_ids(ledger, metadata, findings_doc, errors) + if findings_doc is not None: + check_findings(units, findings_doc, final, incomplete, errors, stats) + if final: + open_work = stats.get("planned", 0) + stats.get("in_progress", 0) + if open_work: + errors.append(f"ledger: {open_work} unit(s) are still planned or " + "in_progress; finish them or mark them deferred " + "with a reason") + return {"valid": not errors, "error_count": len(errors), "errors": errors, + "stats": stats} + + +def build_context(args: argparse.Namespace, metadata: Any) -> Dict[str, Any]: + """Assemble the optional checks requested on the command line.""" + ctx: Dict[str, Any] = {"run_dir": args.run_dir, "block_index": None, + "companions": None} + if not args.no_resolve_blocks: + index = load_block_index(args.references) + if not index: + raise AuditInputError( + f"no reference files in {args.references}; pass --references " + "or --no-resolve-blocks") + ctx["block_index"] = index + if isinstance(metadata, dict) and isinstance(metadata.get("companions"), list): + ctx["companions"] = metadata["companions"] + return ctx + + +def main() -> None: + """Parse arguments, validate the ledger, and set the exit code.""" + parser = argparse.ArgumentParser( + description="Validate coverage-ledger.json from a security audit run.") + parser.add_argument("ledger", type=Path, help="path to coverage-ledger.json") + parser.add_argument("--metadata", type=Path, + help="run-metadata.json; confines companion blocks to " + "the run's selected companions") + parser.add_argument("--findings", type=Path, + help="findings.json; cross-links records and units") + parser.add_argument("--run-dir", type=Path, + help="run directory; verifies promoted evidence files exist") + parser.add_argument("--references", type=Path, default=references_dir(), + help="directory holding the attack-class reference files") + parser.add_argument("--no-resolve-blocks", action="store_true", + help="skip checking block references against headings") + parser.add_argument("--final", action="store_true", + help="pre-report gate: no open work, every candidate decided") + parser.add_argument("--format", choices=["text", "json"], default="text", + help="output format (default: text)") + args = parser.parse_args() + try: + ledger = load_json(args.ledger) + metadata = load_json(args.metadata) if args.metadata else None + findings_doc = load_json(args.findings) if args.findings else None + ctx = build_context(args, metadata) + except AuditInputError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + sys.exit(2) + report = validate(ledger, ctx, metadata, findings_doc, args.final) + report["file"] = str(args.ledger) + emit(report, args.format) + sys.exit(0 if report["valid"] else 1) + + +if __name__ == "__main__": + main() diff --git a/engineering/security-audit/scripts/validate_findings.py b/engineering/security-audit/scripts/validate_findings.py new file mode 100644 index 00000000..42f309f8 --- /dev/null +++ b/engineering/security-audit/scripts/validate_findings.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +"""Validate an audit run's findings.json before it is reported. + +Checks the document against assets/findings.schema.json, then enforces the +rules a schema cannot express: fingerprint ordering and uniqueness, safe +repository-relative paths, trace shape, the severity ceiling, and a usable +resolution plan on every unresolved lead. + +Exit codes: 0 valid, 1 invalid, 2 unreadable input or usage error. + +Usage: + python3 validate_findings.py findings.json + python3 validate_findings.py findings.json --format json + python3 validate_findings.py findings.json --schema custom.schema.json +""" + +import argparse +import re +import sys +from pathlib import Path +from typing import Any, Dict, List + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from audit_common import ( # noqa: E402 + SEVERITIES, AuditInputError, emit, is_repo_path, is_text, load_json) + +TYPES = {"object": dict, "array": list, "string": str, "integer": int, + "boolean": bool} + + +def default_schema() -> Path: + """Path of the schema shipped with the skill.""" + return Path(__file__).resolve().parent.parent / "assets" / "findings.schema.json" + + +def resolve(node: Dict[str, Any], root: Dict[str, Any]) -> Dict[str, Any]: + """Follow a local `#/$defs/name` reference.""" + ref = node.get("$ref") + if not ref: + return node + return root.get("$defs", {}).get(ref.rsplit("/", 1)[-1], {}) + + +def pick_branch(value: Any, branches: List[Dict[str, Any]], + root: Dict[str, Any], where: str, errors: List[str]) -> None: + """Validate against the branch whose verdict matches the record.""" + verdict = value.get("verdict") if isinstance(value, dict) else None + known = [] + for branch in branches: + schema = resolve(branch, root) + const = schema.get("properties", {}).get("verdict", {}).get("const") + known.append(const) + if const == verdict: + check(value, schema, root, where, errors) + return + errors.append(f"{where}.verdict: expected one of {known}, got {verdict!r}") + + +def check(value: Any, node: Dict[str, Any], root: Dict[str, Any], + where: str, errors: List[str]) -> None: + """Validate a value against the schema subset this skill uses.""" + schema = resolve(node, root) + if "oneOf" in schema: + pick_branch(value, schema["oneOf"], root, where, errors) + return + expected = schema.get("type") + if expected: + kind = TYPES[expected] + if not isinstance(value, kind) or (expected == "integer" + and isinstance(value, bool)): + errors.append(f"{where}: expected {expected}, got {type(value).__name__}") + return + if "const" in schema and value != schema["const"]: + errors.append(f"{where}: must equal {schema['const']!r}") + if "enum" in schema and value not in schema["enum"]: + errors.append(f"{where}: {value!r} is not one of {schema['enum']}") + if isinstance(value, str): + if schema.get("minLength") and not is_text(value): + errors.append(f"{where}: must contain visible text") + if "pattern" in schema and not re.match(schema["pattern"], value): + errors.append(f"{where}: does not match {schema['pattern']}") + if isinstance(value, int) and "minimum" in schema and value < schema["minimum"]: + errors.append(f"{where}: must be at least {schema['minimum']}") + if isinstance(value, list): + if len(value) < schema.get("minItems", 0): + errors.append(f"{where}: needs at least {schema['minItems']} item(s)") + for index, item in enumerate(value): + check(item, schema.get("items", {}), root, f"{where}[{index}]", errors) + if isinstance(value, dict): + props = schema.get("properties", {}) + for name in schema.get("required", []): + if name not in value: + errors.append(f"{where}: missing required field {name!r}") + for name, item in value.items(): + if name in props: + check(item, props[name], root, f"{where}.{name}", errors) + elif schema.get("additionalProperties") is False: + errors.append(f"{where}: field {name!r} is not allowed here") + + +def check_trace(record: Dict[str, Any], where: str, errors: List[str]) -> None: + """A trace starts at an entry, ends at a sink, and flows in between.""" + trace = record.get("trace") + if not isinstance(trace, list) or not trace: + return + steps = [step.get("step") if isinstance(step, dict) else None for step in trace] + if len(steps) == 1: + if steps[0] not in ("entry", "sink"): + errors.append(f"{where}.trace[0].step: a one-step trace is 'entry' or 'sink'") + return + if steps[0] != "entry": + errors.append(f"{where}.trace[0].step: must be 'entry'") + if steps[-1] != "sink": + errors.append(f"{where}.trace[{len(steps) - 1}].step: must be 'sink'") + for index, step in enumerate(steps[1:-1], start=1): + if step != "flow": + errors.append(f"{where}.trace[{index}].step: must be 'flow'") + + +def check_paths(record: Dict[str, Any], where: str, errors: List[str]) -> None: + """Every cited file must be a path inside the repository.""" + groups = [("trace", record.get("trace")), ("evidence", record.get("evidence"))] + fix = record.get("fix") + if isinstance(fix, dict): + groups.append(("fix.changes", fix.get("changes"))) + for name, entries in groups: + if not isinstance(entries, list): + continue + for index, entry in enumerate(entries): + if isinstance(entry, dict) and "file" in entry \ + and not is_repo_path(entry["file"]): + errors.append(f"{where}.{name}[{index}].file: must be a " + "repository-relative path with no '..' or leading '/'") + + +def check_confirmed(record: Dict[str, Any], where: str, errors: List[str]) -> None: + """Severity may not outrun what the reproduction demonstrated.""" + severity = record.get("severity") + if not isinstance(severity, dict): + return + overall = severity.get("overall") + impact = (severity.get("impact") or {}).get("level") \ + if isinstance(severity.get("impact"), dict) else None + if overall in SEVERITIES and impact in SEVERITIES \ + and SEVERITIES.index(overall) > SEVERITIES.index(impact): + errors.append(f"{where}.severity.overall: {overall!r} exceeds " + f"demonstrated impact {impact!r}") + confidence = record.get("confidence") + level = confidence.get("level") if isinstance(confidence, dict) else None + if overall in ("high", "critical") and level == "low": + errors.append(f"{where}.confidence.level: a {overall} finding held with " + "low confidence is an unresolved lead; record it as " + "needs_validation or lower the severity") + + +def check_semantics(findings: List[Any], errors: List[str]) -> None: + """Rules that span records or depend on the verdict.""" + seen: Dict[str, int] = {} + previous = None + for index, record in enumerate(findings): + if not isinstance(record, dict): + continue + where = f"findings[{index}]" + fingerprint = record.get("fingerprint") + if isinstance(fingerprint, str): + if fingerprint in seen: + errors.append(f"{where}.fingerprint: duplicates findings" + f"[{seen[fingerprint]}]; one record per root cause") + seen.setdefault(fingerprint, index) + if previous is not None and fingerprint < previous: + errors.append(f"{where}.fingerprint: records must be sorted " + "by fingerprint") + previous = fingerprint + check_trace(record, where, errors) + check_paths(record, where, errors) + verdict = record.get("verdict") + if verdict == "confirmed": + check_confirmed(record, where, errors) + elif verdict == "needs_validation": + plan = record.get("resolution") + if isinstance(plan, dict) and not (is_text(plan.get("local")) + or is_text(plan.get("owner_check"))): + errors.append(f"{where}.resolution: give a 'local' check, an " + "'owner_check', or both") + + +def validate(document: Any, schema: Dict[str, Any]) -> Dict[str, Any]: + """Run every check and return a report with errors and verdict counts.""" + errors: List[str] = [] + check(document, schema, schema, "$", errors) + findings = document.get("findings") if isinstance(document, dict) else None + stats: Dict[str, int] = {} + if isinstance(findings, list): + check_semantics(findings, errors) + for record in findings: + verdict = record.get("verdict") if isinstance(record, dict) else None + if isinstance(verdict, str): + stats[verdict] = stats.get(verdict, 0) + 1 + stats["total"] = len(findings) + return {"valid": not errors, "error_count": len(errors), "errors": errors, + "stats": stats} + + +def main() -> None: + """Parse arguments, validate the findings file, and set the exit code.""" + parser = argparse.ArgumentParser( + description="Validate findings.json from a security audit run.") + parser.add_argument("findings", type=Path, help="path to findings.json") + parser.add_argument("--schema", type=Path, default=default_schema(), + help="schema file (default: the skill's findings.schema.json)") + parser.add_argument("--format", choices=["text", "json"], default="text", + help="output format (default: text)") + args = parser.parse_args() + try: + schema = load_json(args.schema) + document = load_json(args.findings) + except AuditInputError as exc: + print(f"ERROR: {exc}", file=sys.stderr) + sys.exit(2) + report = validate(document, schema) + report["file"] = str(args.findings) + emit(report, args.format) + sys.exit(0 if report["valid"] else 1) + + +if __name__ == "__main__": + main() diff --git a/mkdocs.yml b/mkdocs.yml index 4fd8bf4d..05de213f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,13 +1,13 @@ site_name: "Claude Skills — Universal AI Skills Library" site_url: https://borghei.github.io/Claude-Skills site_description: >- - 372 production-ready AI skills across 20 domains. Project Management + 385 production-ready AI skills across 21 domains. Project Management is the most-used domain (70 skills: discovery → delivery → career, strategy frameworks, GTM, with Jira / Linear / Notion / Confluence integration). Plus engineering, marketing, C-level (incl. CAIO / CDO / CCO / GC / VPE), compliance with audit-prep, research and research-ops, business operations, markdown→HTML publishing, and 7 vertical advisors. - 76 cs-* agents, 867 Python tools. Works with Claude Code, Cursor, Codex, + 77 cs-* agents, 900 Python tools. Works with Claude Code, Cursor, Codex, Copilot, Windsurf, Cline, Gemini CLI, Aider, and Goose. site_author: Amin Borghei repo_url: https://github.com/borghei/Claude-Skills @@ -121,6 +121,7 @@ nav: - Research Ops: skills/research-ops.md - Business Operations: skills/business-operations.md - Markdown & HTML: skills/markdown-html.md + - Tools (LinkedIn): skills/tools.md - Data, HR & Other: skills/other.md - Agents: - agents/index.md diff --git a/registry.json b/registry.json index ea86c598..d7fa0f1d 100644 --- a/registry.json +++ b/registry.json @@ -1,9 +1,9 @@ { "schema_version": "1.0.0", - "generated_at": "2026-09-27T21:13:15+00:00", + "generated_at": "2026-10-07T00:46:25+00:00", "repository": "claude-skills", - "skill_count": 372, - "domain_count": 20, + "skill_count": 385, + "domain_count": 21, "domains": [ "business-growth", "business-operations", @@ -23,6 +23,7 @@ "research", "research-ops", "sales-success", + "tools", "vertical-advisors", "workflow" ], @@ -954,6 +955,23 @@ "has_scripts": true, "has_references": true }, + { + "name": "security-audit", + "domain": "engineering", + "description": "Multi-agent source security audit with a coverage ledger, independently verified findings, and machine-checked records. Use when asked to audit a codebase, find vulnerabilities, or run a source-level pen test.", + "tags": [ + "security-audit", + "vulnerability-research", + "multi-agent", + "code-review", + "appsec" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "path": "engineering/security-audit", + "has_scripts": true, + "has_references": true + }, { "name": "self-improving-agent", "domain": "engineering", @@ -5452,6 +5470,211 @@ "has_scripts": true, "has_references": true }, + { + "name": "linkedin-comment-writer", + "domain": "tools", + "description": "Drafts comments on other people's LinkedIn posts that add a specific the post lacked, then gates them offline. Use when commenting on a post, engaging with a prospect or peer, or checking a comment before pasting it.", + "tags": [ + "linkedin", + "comments", + "engagement", + "social-selling", + "writing" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "path": "tools/linkedin/linkedin-comment-writer", + "has_scripts": true, + "has_references": true + }, + { + "name": "linkedin-content-planner", + "domain": "tools", + "description": "Builds a weekly or monthly LinkedIn publishing plan from content pillars, a posting cadence and a format mix, with a founder-oriented pillar set as an alternative to the general one. Use when planning a week, a month or a launch period of posts rather than drafting a single post.", + "tags": [ + "linkedin", + "content-calendar", + "content-pillars", + "editorial-planning", + "founder-content" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "path": "tools/linkedin/linkedin-content-planner", + "has_scripts": true, + "has_references": true + }, + { + "name": "linkedin-content-repurposer", + "domain": "tools", + "description": "Turns something made for another channel (a thread, a video or talk transcript, a blog post, a newsletter) into a post that reads as native to LinkedIn, without changing what the source says. Use when existing material should become a LinkedIn post rather than writing one from a blank page.", + "tags": [ + "linkedin", + "repurposing", + "content-adaptation", + "transcripts", + "fidelity-check" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "path": "tools/linkedin/linkedin-content-repurposer", + "has_scripts": true, + "has_references": true + }, + { + "name": "linkedin-employee-advocacy", + "domain": "tools", + "description": "Designs and runs an employee advocacy programme: who posts what and how often, review rules, and what is never scripted. Use when getting a team posting, writing an advocacy policy, or fixing a programme that has stalled.", + "tags": [ + "linkedin", + "employee-advocacy", + "governance", + "team-cadence", + "social-policy" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "path": "tools/linkedin/linkedin-employee-advocacy", + "has_scripts": true, + "has_references": true + }, + { + "name": "linkedin-engagement-analytics", + "domain": "tools", + "description": "Segments who reacted to and commented on a post from a CSV or JSON export, and says whether it reached the intended audience. Use when reviewing a post's engagers, checking audience fit, or building a baseline.", + "tags": [ + "linkedin", + "analytics", + "audience-segmentation", + "engagement", + "privacy" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "path": "tools/linkedin/linkedin-engagement-analytics", + "has_scripts": true, + "has_references": true + }, + { + "name": "linkedin-hook-analyzer", + "domain": "tools", + "description": "Extracts and classifies the opening-line pattern of LinkedIn posts the user pastes or saves, and turns each into a reusable slot template with cautions. Use when studying why an opening works, building a swipe file, or adapting a hook pattern without copying it.", + "tags": [ + "linkedin", + "hooks", + "swipe-file", + "pattern-analysis", + "copywriting" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "path": "tools/linkedin/linkedin-hook-analyzer", + "has_scripts": true, + "has_references": true + }, + { + "name": "linkedin-humanizer", + "domain": "tools", + "description": "Audits and rewrites LinkedIn drafts to remove machine-sounding patterns: tiered tell catalogue, emoji-pattern scoring, rule explanations, and a voice fingerprint built from the author's own posts. Use when a draft reads generated, before publishing, or when an edit flattened someone's voice.", + "tags": [ + "linkedin", + "humanizer", + "ai-writing", + "editing", + "voice", + "pre-publish" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "path": "tools/linkedin/linkedin-humanizer", + "has_scripts": true, + "has_references": true + }, + { + "name": "linkedin-post-writer", + "domain": "tools", + "description": "Drafts LinkedIn posts from a brief: picks an angle and opening pattern the author's real material supports, structures the body, and gates the draft before it is pasted. Use when writing a post, choosing a hook, or checking a draft before publishing.", + "tags": [ + "linkedin", + "copywriting", + "hooks", + "content-drafting", + "pre-publish" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "path": "tools/linkedin/linkedin-post-writer", + "has_scripts": true, + "has_references": true + }, + { + "name": "linkedin-profile-optimizer", + "domain": "tools", + "description": "Audits and rewrites a LinkedIn profile from pasted text: headline, About, experience, skills, Featured, banner and photo. Use when reviewing a profile, fixing a headline or About, or preparing a profile before posting more.", + "tags": [ + "linkedin", + "profile", + "headline", + "personal-branding", + "career" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "path": "tools/linkedin/linkedin-profile-optimizer", + "has_scripts": true, + "has_references": true + }, + { + "name": "linkedin-reply-manager", + "domain": "tools", + "description": "Triages a pasted LinkedIn comment section and drafts replies: which comments to answer, in what order, which to ignore. Use when answering comments on your own post, replying inside a thread, or clearing a backlog.", + "tags": [ + "linkedin", + "replies", + "comment-triage", + "community-management", + "engagement" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "path": "tools/linkedin/linkedin-reply-manager", + "has_scripts": true, + "has_references": true + }, + { + "name": "linkedin-story-interviewer", + "domain": "tools", + "description": "Interviews a person to surface what they actually have to say, and keeps the answers in a local story bank file that later LinkedIn drafts draw on. Use when someone has never posted, when drafts keep coming out generic, or when the story bank is thin or stale.", + "tags": [ + "linkedin", + "interviewing", + "story-bank", + "content-discovery", + "personal-brand" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "path": "tools/linkedin/linkedin-story-interviewer", + "has_scripts": true, + "has_references": true + }, + { + "name": "linkedin-thread-tracker", + "domain": "tools", + "description": "Keeps a local log of LinkedIn comments you left, records who replied, and reports follow-ups due, overdue, or dead. Use when logging a comment, checking which threads need an answer, or reviewing reply rates.", + "tags": [ + "linkedin", + "follow-up", + "thread-tracking", + "engagement", + "relationship-building" + ], + "version": "1.0.0", + "updated": "2026-10-07", + "path": "tools/linkedin/linkedin-thread-tracker", + "has_scripts": true, + "has_references": true + }, { "name": "handoff", "domain": "workflow", diff --git a/scripts/build_manifest.py b/scripts/build_manifest.py index 14ee76a7..8516090e 100644 --- a/scripts/build_manifest.py +++ b/scripts/build_manifest.py @@ -45,6 +45,7 @@ "research-ops", "business-operations", "markdown-html", + "tools", "workflow", ] diff --git a/scripts/generate_site.py b/scripts/generate_site.py index 68aafdf8..1edfd201 100755 --- a/scripts/generate_site.py +++ b/scripts/generate_site.py @@ -50,6 +50,7 @@ "hr-operations": {"label": "HR Operations", "desc": "Talent acquisition, people analytics, HR business partner"}, "finance": {"label": "Finance", "desc": "DCF valuation, budgeting, forecasting, ratio analysis"}, "legal": {"label": "Legal", "desc": "Contract review, NDA, privacy, DPIA, risk, compliance (EXPERIMENTAL)"}, + "tools": {"label": "Tools", "desc": "Platform toolkits: the LinkedIn suite for posts, comments, planning, profile, and analytics"}, } # Inline SVG icons (16x16, stroke-based line icons) diff --git a/site/404.html b/site/404.html index c1b5933a..1fab06bc 100644 --- a/site/404.html +++ b/site/404.html @@ -6,7 +6,7 @@ - + @@ -661,6 +661,8 @@ + + @@ -1034,6 +1036,34 @@ +
  • + + + + + + + + Tools (LinkedIn) + + + + + + + + +
  • + + + + + + + + + +
  • diff --git a/site/AUDIT_REPORT/index.html b/site/AUDIT_REPORT/index.html index 88f292e0..473a532e 100644 --- a/site/AUDIT_REPORT/index.html +++ b/site/AUDIT_REPORT/index.html @@ -6,7 +6,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -70,7 +70,7 @@ - + @@ -681,6 +681,8 @@ + + @@ -1054,6 +1056,34 @@ +
  • + + + + + + + + Tools (LinkedIn) + + + + + + + + +
  • + + + + + + + + + +
  • diff --git a/site/INSTALLATION/index.html b/site/INSTALLATION/index.html index 128f87be..28c79810 100644 --- a/site/INSTALLATION/index.html +++ b/site/INSTALLATION/index.html @@ -6,7 +6,7 @@ - + @@ -62,7 +62,7 @@ - + @@ -70,7 +70,7 @@ - + @@ -681,6 +681,8 @@ + + @@ -1054,6 +1056,34 @@ +
  • + + + + + + + + Tools (LinkedIn) + + + + + + + + +
  • + + + + + + + + + +
  • @@ -2764,7 +2794,7 @@

    Installation Guide - AI Skills Library

    -

    Complete installation guide for all 372 production-ready skills across 20 domains, 867 Python tools, 76 agents, 26 slash commands, 21 compound sub-skills, 18 compliance frameworks and 8 CI/CD workflows. Works with Claude Code, Claude.ai, Cursor, Copilot, Codex, Gemini CLI, Windsurf, Cline, Aider, Goose, ChatGPT and more.

    +

    Complete installation guide for all 385 production-ready skills across 21 domains, 900 Python tools, 77 agents, 26 slash commands, 21 compound sub-skills, 18 compliance frameworks and 8 CI/CD workflows. Works with Claude Code, Claude.ai, Cursor, Copilot, Codex, Gemini CLI, Windsurf, Cline, Aider, Goose, ChatGPT and more.

    Table of Contents