From a0d3c614e2a269277cdd8a28e9b56535cfd702f3 Mon Sep 17 00:00:00 2001 From: Malik Dixon Date: Sun, 16 Aug 2026 18:50:51 -0400 Subject: [PATCH] Prepare v0.1.0: MIT license, command entry points, README, and catalog fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release requirements -------------------- - License is now MIT. Replaces PolyForm Noncommercial across LICENSE.md, SKILL.md, README.md, CONTRIBUTING.md, MAINTAINERS.md, the CLA relicensing clause, and the issue-template contact links. Removes docs/legal/COMMERCIAL-LICENSING.md, which described a restriction that no longer exists. TRADEMARKS.md now states the boundary explicitly, since it used to lean on the noncommercial license: the code is MIT, the DSB names, logos, and curriculum are not. - Adds /devsecops-engineer:advise, :design, and :assess via .claude-plugin/plugin.json and commands/. Each command selects an operating mode, passes $ARGUMENTS through, and reads SKILL.md — nothing more. A command that restates a rule creates a second catalog nobody validates, so tests/test_commands.py fails any command containing a rule ID or an enforcement level, and the policy is written down in CONTRIBUTING.md. - README rewritten as a product front door: what it is, why (four outcomes), 60-second install, three runnable prompts, workflows, how the reasoning works, documentation index, status. Adds a table of contents and links every file under docs/ — docs/legal/CLA.md had never been reachable from it. tests/test_readme.py holds both properties. - Adds CHANGELOG.md with the v0.1.0 entry and explicit pre-1.0 language. A test asserts plugin.json and the changelog agree on the version, so they cannot diverge at tag time. - Moves project documentation into docs/. LICENSE.md stays at the root, where GitHub looks for it. Correctness fixes ----------------- - Re-keys 21 rules onto capabilities that describe what actually satisfies them, and adds the 10 capabilities they needed. Resolution keys on capability, so DSB-SRC-001, DSB-ID-002, DSB-EVD-001 and others previously resolved to REUSE the moment an organization declared any pipeline-configuration scanner — reporting branch protection and least privilege as satisfied by a tool that does neither. Likewise sbom-generation on DSB-BUILD-001, and secret-scanning on DSB-ID-003, which requires a secret store rather than a detector. - Fixes the GitHub Actions reference pipeline's DSB-EXC-003 assertion, whose grep pattern contained the literal string it searched for and so failed on a clean repository. Patterns now live in env, bracket-escaped, and allow a line annotated `# DSB-WARN:` because continue-on-error is how this platform expresses a WARN-level control. - Replaces `aws ecs deploy`, which is not an AWS CLI command, with describe/register/update-service by digest; adds the ECR registry and login that DSB-ART-001 requires; separates the publish role from the deployment roles; gates the scan jobs on test per DSB-TEST-001. - Pins the Jenkins shared library to a commit. `@v3` is a mutable reference and is the defect DSB-SC-002 exists to catch. - Adds five Workload Profile fields that SKILL.md documents and rules depend on but the schema rejected, and lets array fields express `unknown`. Two of the three published example profiles failed their own schema. - Pins tools/requirements.txt exactly. `>=` floors mean a commit does not resolve a fixed dependency set, which is what DSB-BUILD-002 forbids. - Corrects TESTING.md's validator demonstration, which targeted a string not present in SKILL.md and therefore proved nothing, and CONTRIBUTING.md's documented rule format, which omitted the curriculum module title the validator requires. Verification: validate_skill.py reports 42 rules across 11 families with no violations, the generated mapping index is current, and the suite is at 106 tests, up from 56. Co-Authored-By: Claude Opus 5 (1M context) --- .claude-plugin/plugin.json | 24 ++ .github/CODEOWNERS | 12 +- .github/ISSUE_TEMPLATE/bug_report.yml | 2 +- .github/ISSUE_TEMPLATE/config.yml | 6 +- .github/workflows/validate.yml | 10 + CHANGELOG.md | 44 +++ LICENSE.md | 104 ++----- README.md | 293 ++++++++++++------ SKILL.md | 60 ++-- commands/advise.md | 25 ++ commands/assess.md | 23 ++ commands/design.md | 22 ++ CODE_OF_CONDUCT.md => docs/CODE_OF_CONDUCT.md | 0 CONTRIBUTING.md => docs/CONTRIBUTING.md | 38 ++- MAINTAINERS.md => docs/MAINTAINERS.md | 6 +- SECURITY.md => docs/SECURITY.md | 0 TESTING.md => docs/TESTING.md | 24 +- {references => docs}/framework-mappings.md | 0 docs/legal/CLA.md | 2 +- docs/legal/COMMERCIAL-LICENSING.md | 52 ---- docs/legal/TRADEMARKS.md | 2 +- examples/advise-terraform-only/README.md | 18 +- examples/generate-github-actions/README.md | 48 ++- examples/generate-github-actions/delivery.yml | 120 ++++++- examples/generate-jenkins/Jenkinsfile | 7 +- examples/generate-jenkins/README.md | 2 +- references/capabilities.yaml | 66 ++++ references/dsb-curriculum.yaml | 2 +- rules/README.md | 2 +- rules/build/DSB-BUILD-001.yaml | 4 +- schema/workload-profile.schema.json | 43 ++- tests/test_commands.py | 98 ++++++ tests/test_curriculum.py | 10 +- tests/test_examples.py | 26 +- tests/test_mappings.py | 2 +- tests/test_profile_schema.py | 60 ++++ tests/test_readme.py | 61 ++++ tools/generate_mappings.py | 6 +- tools/requirements.txt | 16 +- 39 files changed, 996 insertions(+), 344 deletions(-) create mode 100644 .claude-plugin/plugin.json create mode 100644 CHANGELOG.md create mode 100644 commands/advise.md create mode 100644 commands/assess.md create mode 100644 commands/design.md rename CODE_OF_CONDUCT.md => docs/CODE_OF_CONDUCT.md (100%) rename CONTRIBUTING.md => docs/CONTRIBUTING.md (78%) rename MAINTAINERS.md => docs/MAINTAINERS.md (89%) rename SECURITY.md => docs/SECURITY.md (100%) rename TESTING.md => docs/TESTING.md (90%) rename {references => docs}/framework-mappings.md (100%) delete mode 100644 docs/legal/COMMERCIAL-LICENSING.md create mode 100644 tests/test_commands.py create mode 100644 tests/test_readme.py diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..4ca77a7 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,24 @@ +{ + "name": "devsecops-engineer", + "version": "0.1.0", + "description": "Advise on, design, and assess CI/CD delivery pipelines using The DevSec Blueprint's Build → Test → Scan → Deploy DevSecOps engineering methodology.", + "author": { + "name": "The DevSec Blueprint", + "url": "https://github.com/devsecblueprint" + }, + "homepage": "https://github.com/devsecblueprint/devsecops-claude-skill", + "repository": "https://github.com/devsecblueprint/devsecops-claude-skill", + "license": "MIT", + "keywords": [ + "devsecops", + "cicd", + "pipeline", + "appsec", + "supply-chain", + "sast", + "sca", + "sbom", + "ssdf", + "slsa" + ] +} diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fae3123..df04969 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,17 +1,21 @@ # Ownership for The DSB DevSecOps Engineering Skill. -# See MAINTAINERS.md. Later matches win, so the narrow rules come last. +# See docs/MAINTAINERS.md. Later matches win, so the narrow rules come last. * @damienjburks # SKILL.md is the product. Every change to it is a change to guidance that -# reaches users directly. +# reaches users directly. commands/ and the plugin manifest define the public +# entry points and the released version. /SKILL.md @damienjburks +/commands/ @damienjburks +/.claude-plugin/ @damienjburks +/CHANGELOG.md @damienjburks # Rule content and the registries rules are validated against. /rules/ @damienjburks /references/ @damienjburks /schema/ @damienjburks -# Licensing and contributor terms. +# Project documentation, licensing, and contributor terms. +/docs/ @damienjburks /LICENSE.md @damienjburks -/docs/legal/ @damienjburks diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml index 11474bd..dcf0dd1 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.yml +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -7,7 +7,7 @@ body: value: | If the problem is that generated configuration would introduce a real security weakness, report it privately instead — see - [SECURITY.md](https://github.com/devsecblueprint/devsecops-claude-skill/blob/main/SECURITY.md). + [SECURITY.md](https://github.com/devsecblueprint/devsecops-claude-skill/blob/main/docs/SECURITY.md). - type: dropdown id: area diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 13e9720..64d67b7 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -12,6 +12,6 @@ contact_links: url: https://github.com/devsecblueprint/devsecblueprint/issues/new about: The four phases and baseline principles come from DSB curriculum — raise those on the platform repository. - - name: Commercial licensing - url: https://github.com/devsecblueprint/devsecops-claude-skill/blob/main/docs/legal/COMMERCIAL-LICENSING.md - about: Using this inside a company requires prior written authorization. + - name: Trademark and brand use + url: https://github.com/devsecblueprint/devsecops-claude-skill/blob/main/docs/legal/TRADEMARKS.md + about: The code is MIT. DSB names, logos, and curriculum content are not. diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index c81fd84..3c97990 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -20,11 +20,21 @@ jobs: with: python-version: "3.13" + # DSB-BUILD-002: requirements.txt pins direct dependencies exactly, so a + # given commit resolves the same versions of the tools CI runs. Transitive + # versions still float — a fully hashed lock file is the next step up, and + # is only worth it if this repository starts shipping an executable + # artifact rather than Markdown. - name: Install dependencies run: pip install -r tools/requirements.txt - name: Validate SKILL.md run: python tools/validate_skill.py + # DSB-EVD-001: the generated index must match SKILL.md. tests/ covers this + # too; running it here names the fix in the failure output. + - name: Check generated mappings are current + run: python tools/generate_mappings.py --check + - name: Run tests run: python -m pytest -v diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..b6e2a0d --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,44 @@ +# Changelog + +All notable changes to this project are documented here. This project follows +[Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [0.1.0] — 2026-08-16 + +Initial public release. + +**Pre-1.0.** The rule catalog and the skill's output structure will evolve based on +real usage before 1.0. Rule *IDs* are the exception and are already stable: they +appear in review output that ends up in audit records, so a retired ID is deprecated, +never reused or renumbered. + +### Added + +- `SKILL.md` — the skill. Self-contained: 42 rules across 11 families, the + Build → Test → Scan → Deploy methodology, the twenty baseline principles, and the + Advise / Design / Review operating modes. Installing it needs this file and nothing + else. +- Command entry points for Claude Code, under the `devsecops-engineer` plugin: + - `/devsecops-engineer:advise` — where controls belong, given a stack and the tools + already owned + - `/devsecops-engineer:design` — implementation-ready pipeline configuration + - `/devsecops-engineer:assess` — findings against an existing pipeline or repository +- Worked examples across four stacks — greenfield GitHub Actions, an enterprise + Jenkins toolchain that introduces zero new scanners, a Terraform-only repository + where twelve rules are Not Applicable, and a review of a pipeline with real defects. +- Framework mappings to NIST SSDF, SLSA, OWASP CI/CD, OWASP SAMM, and CNCF supply + chain guidance, generated from the rule catalog into `docs/framework-mappings.md`. +- Machine-readable capability registry, curriculum snapshot, JSON Schemas for the rule + and workload-profile structures, and a partial YAML projection of the catalog. +- `tools/validate_skill.py` and a pytest suite, both run in CI. + +### Changed + +- **License is now MIT** (was PolyForm Noncommercial 1.0.0). Commercial use no longer + requires prior written authorization, and `docs/legal/COMMERCIAL-LICENSING.md` has + been removed. DSB names, logos, and curriculum content remain outside the license — + see `docs/legal/TRADEMARKS.md`. +- README rewritten as a product front door: what it is, why it exists, install, three + prompts to try, and how the reasoning works. + +[0.1.0]: https://github.com/devsecblueprint/devsecops-claude-skill/releases/tag/v0.1.0 diff --git a/LICENSE.md b/LICENSE.md index 87f7759..f6f521d 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,83 +1,21 @@ -# PolyForm Noncommercial License 1.0.0 - - - -Required Notice: Copyright © 2026 The DevSec Blueprint LLC. All rights reserved. - -Required Notice: Commercial use of this software requires prior written authorization from The DevSec Blueprint LLC. - -Required Notice: The DevSec Blueprint name, DSB name, logos, trademarks, service marks, and branding are not licensed under these terms. - -## Acceptance - -In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses. - -## Copyright License - -The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to Distribution License and make changes or new works based on the software according to Changes and New Works License. - -## Distribution License - -The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by Changes and New Works License. - -## Notices - -You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example: - -> Required Notice: Copyright Yoyodyne, Inc. (http://example.com) - -## Changes and New Works License - -The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose. - -## Patent License - -The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software. - -## Noncommercial Purposes - -Any noncommercial purpose is a permitted purpose. - -## Personal Uses - -Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose. - -## Noncommercial Organizations - -Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding. - -## Fair Use - -You may have "fair use" rights for the software under the law. These terms do not limit them. - -## No Other Rights - -These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses. - -## Patent Defense - -If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. - -## Violations - -The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately. - -## No Liability - -As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim. - -## Definitions - -The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms. - -**You** refers to the individual or entity agreeing to these terms. - -**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. - -**Your licenses** are all the licenses granted to you for the software under these terms. - -**Use** means anything you do with the software requiring one of your licenses. - ---- - -PolyForm Noncommercial License 1.0.0 is a standard license published by the PolyForm Project. The required notices above identify The DevSec Blueprint LLC and state conditions applicable to DSB-owned software distributed with this file. +# MIT License + +Copyright (c) 2026 The DevSec Blueprint LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index 787de29..038d190 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,78 @@ # DSB DevSecOps Engineering Skill -An AI-agent skill implementing The DevSec Blueprint's DevSecOps engineering -methodology. It advises on, designs, generates, and reviews CI/CD delivery pipelines. +**Turn a description of your stack into a delivery pipeline with the right security +controls in the right places — and no scanners you did not need.** -> **Build what you need. Test what you built. Scan what can introduce meaningful -> risk. Deploy only what passed.** +By [The DevSec Blueprint](https://github.com/devsecblueprint) · MIT · +**v0.1.0 (pre-1.0)** -**The DevSec Blueprint defines the required capabilities and engineering outcomes. -Your organization determines how those capabilities are implemented.** +> Build what you need. Test what you built. Scan what can introduce meaningful risk. +> Deploy only what passed. --- -## Licensing +## Contents + +- [What is this?](#what-is-this) +- [Why use it](#why-use-it) +- [Install in 60 seconds](#install-in-60-seconds) +- [Try it](#try-it) +- [Common workflows](#common-workflows) +- [How it works](#how-it-works) + - [The rule catalog](#the-rule-catalog) +- [Documentation](#documentation) + - [Repository contents](#repository-contents) +- [Development](#development) +- [Status](#status) +- [License and ownership](#license-and-ownership) -PolyForm Noncommercial License 1.0.0. +--- + +## What is this? -**Commercial use requires prior written authorization from The DevSec Blueprint LLC.** -See [Commercial Licensing](docs/legal/COMMERCIAL-LICENSING.md). +A skill for AI coding agents that encodes how The DevSec Blueprint teaches DevSecOps +engineering. Give it your stack — or point it at a pipeline you already have — and it +reasons from a fixed rule catalog to a **Control Plan**: which security controls apply +to *this* workload, which are already covered by tools you own, where each one belongs +in the pipeline, and whether it should block delivery or just report. -Free for noncommercial use — learning, personal projects, and evaluation. If you want -to use this inside a company, start with the commercial licensing process. +It is not a pipeline generator with a scanner list bolted on. It decides what applies +before it writes anything. -The license covers the software in this repository only. It grants no rights to DSB -curriculum, walkthroughs, training materials, name, logos, or other branded assets. -See [Trademarks](docs/legal/TRADEMARKS.md). +**DSB defines the required capabilities and engineering outcomes. Your organization +determines how those capabilities are implemented.** --- -## Install +## Why use it + +**It reuses what you already own.** Tell it you have Checkmarx and Black Duck and it +resolves SAST and SCA to *satisfied*, by name, and moves on. It does not propose Snyk +alongside Black Duck. Zero new scanners is a correct answer, and the most common one +in an enterprise. + +**It says what does not apply, and why.** A Terraform-only repository gets container +scanning marked Not Applicable with the stated fact that produced it — not silently +dropped, and not recommended "to be safe". Twelve explicit N/A determinations are the +deliverable, not filler. + +**It asks instead of assuming.** Partial context produces a partial plan plus the +specific unresolved fields, phrased as questions. It does not infer that you build +containers because you mentioned Java. + +**It explains the engineering.** Every recommendation carries a rule ID, the reason +that placement is the earliest technically valid one, and whether the enforcement +level came from DSB or from your own policy. -The entire skill is one file. There is nothing to build and no dependencies. +**It refuses the dangerous default.** Active DAST against production requires explicit +authorization. Unknown is treated as no. -**Claude Code:** +--- + +## Install in 60 seconds + +**As a skill** — one file, no dependencies, works in any agent that loads Markdown +instructions: ```bash mkdir -p ~/.claude/skills/dsb-devsecops @@ -39,80 +80,113 @@ curl -o ~/.claude/skills/dsb-devsecops/SKILL.md \ https://raw.githubusercontent.com/devsecblueprint/devsecops-claude-skill/main/SKILL.md ``` -Restart your agent, then ask it to design or review a pipeline. It loads on its own -when the topic matches — you do not need to name it. +Restart your agent. It loads on its own when the topic matches — you never name it. -**Other agents:** drop `SKILL.md` wherever your tool loads instructions from, or paste -its contents into the system prompt. It is plain Markdown with YAML frontmatter and no -external references. +**As a Claude Code plugin**, if you also want the `/devsecops-engineer:*` commands: ---- +```bash +/plugin install devsecblueprint/devsecops-claude-skill +``` -## What it does +**Any other agent:** drop `SKILL.md` wherever your tool loads instructions from, or +paste it into the system prompt. Plain Markdown with YAML frontmatter, no external +references. -Three operating modes, all driven by the same reasoning procedure so they cannot -contradict each other: +--- -| Mode | Use when | -|---|---| -| **Advise** | You describe your stack and constraints and want architecture guidance | -| **Design** | You want implementation-ready pipeline configuration | -| **Review** | You have a pipeline, workflow, or repository to assess against DSB requirements | +## Try it + +Three prompts, one per operating mode. Slash commands are shown where the plugin is +installed; the plain-language version does the same thing with the skill alone. -Repository access is never required. Given partial context it produces a partial plan -plus the specific questions it needs answered — it does not guess. +**Advise — where do controls belong, given what we already own?** -### Capability-based, not vendor-based +``` +/devsecops-engineer:advise We use Jenkins, Java, Maven, Artifactory, Checkmarx, +Black Duck, Prisma Cloud, and OpenShift. How should our pipeline be designed? +``` -DSB requires Software Composition Analysis. It does not require Snyk, Black Duck, -Dependabot, or Trivy. +You should get Checkmarx → SAST, Black Duck → SCA, Prisma → container scanning, all +resolved as reuse; SBOM generation flagged as a real gap; and questions about the +things it genuinely cannot know. Compare with +[`examples/generate-jenkins/`](examples/generate-jenkins/). -If you already have a capability, the skill **uses it** rather than recommending a -replacement: +**Design — give me the pipeline.** ``` -Required capability: Software Composition Analysis -Existing tool: Black Duck -Decision: REUSE — Black Duck satisfies this in the Scan phase -DSB requirement: Satisfied +/devsecops-engineer:design A Node.js service that builds a container and deploys to +AWS ECS. Terraform for infrastructure, staging and production. ``` -Tool names anywhere in this repository are illustrative examples. Presenting one as a -DSB requirement is a defect. +Container scan before push, deploy by digest, OIDC instead of static keys, actions +pinned to SHAs, DAST against staging only. Compare with +[`examples/generate-github-actions/delivery.yml`](examples/generate-github-actions/delivery.yml). + +**Assess — what is wrong with the pipeline we have?** -### No bloat +``` +/devsecops-engineer:assess .github/workflows/deploy.yml +``` + +Findings ordered by what an attacker reaches first. Compare with +[`examples/review-non-compliant/`](examples/review-non-compliant/). -Controls are workload-driven, not checklist-driven: +--- -- No container scanner when there is no container artifact -- No IaC scanner when there is no infrastructure-as-code -- No Kubernetes scanner when Kubernetes is not used -- No second SCA scanner when one already covers it +## Common workflows -`Not Applicable` is a valid engineering outcome and is always stated with its reason. +| You want to… | Do this | +|---|---| +| Decide which scanners a new service actually needs | Advise with your stack and existing tools | +| Justify *not* buying another scanner | Advise — the reuse resolution names the tool that already covers it | +| Stand up a pipeline for a new repository | Design, after answering the platform/language/target questions | +| Audit a pipeline you inherited | Assess, pointed at the workflow file | +| Prepare for an SSDF or SLSA conversation | Assess, then [`docs/framework-mappings.md`](docs/framework-mappings.md) | +| Explain a control to a team that pushed back | Ask why a rule exists — every rule carries its engineering reason | --- -## The methodology +## How it works + +All three modes run the same procedure and produce the same intermediate artifact, so +Advise, Design, and Assess cannot disagree about what applies to a workload. ``` -BUILD → TEST → SCAN → DEPLOY +Your description, a pipeline file, a repository, or any mix + ↓ +1. WORKLOAD PROFILE only stated facts; everything else is `unknown` + ↓ +2. APPLICABILITY each rule → APPLIES | NOT APPLICABLE (+reason) | UNKNOWN (+question) + ↓ +3. CAPABILITY RESOLUTION reuse existing | delegated elsewhere | gap | duplicate + ↓ +4. ENFORCEMENT + PLACEMENT block/warn/report, at the earliest valid phase + ↓ + ══ CONTROL PLAN ══ + ↓ + Advise (explain) Design (render) Assess (diff against what exists) ``` -Logical engineering phases — not a mandated job structure, vendor, or platform. -Runtime-dependent controls such as DAST execute after deployment to a non-production -environment; that is a placement decision inside Scan, not a fifth phase. +Everything you did not state is `unknown`, which is what lets it work without +repository access — and what turns the unresolved fields into its questions instead +of its assumptions. -The full methodology, the twenty baseline principles, and the rule catalog are in -[`SKILL.md`](SKILL.md). +The four phases: ---- +``` +BUILD → TEST → SCAN → DEPLOY +``` + +Logical engineering phases, not a mandated job structure. One GitHub Actions job can +satisfy all four; twelve Jenkins stages can satisfy the same four. Runtime-dependent +controls such as DAST run after deployment to a non-production environment — that is a +placement decision inside Scan, not a fifth phase. -## Rule catalog +### The rule catalog -42 rules across 11 families, each mapped to recognized industry guidance — NIST SSDF, -SLSA, OWASP CI/CD Security, OWASP SAMM, and CNCF supply chain guidance — and to the -DSB curriculum modules that teach the underlying concept. +42 rules across 11 families, each mapped to NIST SSDF, SLSA, OWASP CI/CD Security, +OWASP SAMM, and CNCF supply chain guidance, and to the DSB curriculum modules that +teach the concept. | Family | Covers | |---|---| @@ -128,32 +202,57 @@ DSB curriculum modules that teach the underlying concept. | `DSB-EVD` | Evidence, logging, observability | | `DSB-EXC` | Exceptions and risk acceptance | -Rule IDs are permanent. A deprecated rule keeps its number forever, because IDs appear -in review output that ends up in audit records. +Rules require **capabilities, never vendors**. Every product name in this repository +is illustrative; presenting one as a DSB requirement is a defect. Your internal or +proprietary tools satisfy rules with no rule changes — declare them against the +capability they provide. -The rules are defined in [`SKILL.md`](SKILL.md) §3. To go the other way — from an SSDF -practice, SLSA level, or OWASP CI/CD risk to the DSB rules that carry it — see -[`references/framework-mappings.md`](references/framework-mappings.md). +Rule IDs are permanent. A retired rule keeps its number forever, because IDs end up in +audit records. --- -## Repository contents +## Documentation + +**Start here** + +| Document | What it covers | +|---|---| +| [`SKILL.md`](SKILL.md) | The skill itself — methodology, twenty baseline principles, all 42 rules, and the three operating modes | +| [`examples/`](examples/) | Worked scenarios across four stacks, including two complete reference pipelines | +| [`CHANGELOG.md`](CHANGELOG.md) | What shipped in each release, and what pre-1.0 means for stability | + +**In [`docs/`](docs/)** + +| Document | What it covers | +|---|---| +| [`docs/framework-mappings.md`](docs/framework-mappings.md) | Framework-first index: an SSDF practice, SLSA level, or OWASP CI/CD risk → the DSB rules that carry it. Generated from `SKILL.md`, never hand-edited | +| [`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) | How to add or change a rule, the vendor-neutrality constraint, the commands-stay-thin policy, and how to refresh the curriculum snapshot | +| [`docs/TESTING.md`](docs/TESTING.md) | The two testing layers — automated consistency checks, and the behavioral tests that prove a change actually moved agent behavior | +| [`docs/MAINTAINERS.md`](docs/MAINTAINERS.md) | Ownership, maintainer responsibilities, and the review and release rules | +| [`docs/SECURITY.md`](docs/SECURITY.md) | How to report a vulnerability, and what counts as one in a repository that ships guidance rather than a running service | +| [`docs/CODE_OF_CONDUCT.md`](docs/CODE_OF_CONDUCT.md) | Community standards, including where vendor advocacy stops being a technical discussion | + +**Legal, in [`docs/legal/`](docs/legal/)** + +| Document | What it covers | +|---|---| +| [`LICENSE.md`](LICENSE.md) | MIT. Commercial use permitted, no separate authorization | +| [`docs/legal/TRADEMARKS.md`](docs/legal/TRADEMARKS.md) | DSB names, logos, and curriculum sit outside the MIT license — what you may and may not call your fork | +| [`docs/legal/CLA.md`](docs/legal/CLA.md) | Contributor License Agreement covering the rights you grant when you submit a change | + +### Repository contents | Path | Purpose | |---|---| | `SKILL.md` | **The skill.** Self-contained — this is all a user needs | +| `commands/` | `/devsecops-engineer:*` entry points — thin wrappers over `SKILL.md` | | `examples/` | Worked scenarios and reference pipelines across four stacks | | `rules/` | Machine-readable projection of the catalog — see [`rules/README.md`](rules/README.md) | -| `references/capabilities.yaml` | Machine-readable capability registry | -| `references/dsb-curriculum.yaml` | Curriculum index snapshot, for validating citations | -| `references/framework-mappings.md` | Framework-first index — generated, not hand-edited | +| `references/` | Capability registry and DSB curriculum snapshot | | `schema/` | JSON Schemas for the rule and workload-profile structures | -| `tools/validate_skill.py` | Consistency checks, run in CI | -| `tools/generate_mappings.py` | Regenerates the framework mapping index | -| `docs/legal/` | Licensing, trademark, and contributor terms | - -[CONTRIBUTING.md](CONTRIBUTING.md) · [SECURITY.md](SECURITY.md) · -[CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md) · [MAINTAINERS.md](MAINTAINERS.md) +| `tools/` | Consistency checks and the mapping-index generator, both run in CI | +| `docs/` | Contributing, testing, project policy, and framework mappings | --- @@ -167,30 +266,48 @@ python tools/validate_skill.py # consistency checks python -m pytest # test suite ``` -Install your working copy locally and iterate: +Install your working copy and iterate: ```bash mkdir -p ~/.claude/skills/dsb-devsecops cp SKILL.md ~/.claude/skills/dsb-devsecops/ - ls ~/.claude/skills/dsb-devsecops/ # SKILL.md and nothing else ``` -See [CONTRIBUTING.md](CONTRIBUTING.md) before opening a pull request. +A `SKILL.md` change that reads well but does not change agent behavior has not landed. +[`docs/TESTING.md`](docs/TESTING.md) covers how to verify that; read +[`docs/CONTRIBUTING.md`](docs/CONTRIBUTING.md) before opening a pull request. --- ## Status -Advise, Design, and Review modes are implemented, with worked -[examples](examples/) for each — including complete rule-annotated reference -pipelines for GitHub Actions and Jenkins. +**v0.1.0 — pre-1.0.** All three modes are implemented, with worked examples for each +and complete rule-annotated reference pipelines for GitHub Actions and Jenkins. + +The catalog and output structure will evolve based on real usage before 1.0. Rule IDs +are already stable. See [`CHANGELOG.md`](CHANGELOG.md). + +Feedback from real pipelines is the most useful thing you can contribute right now — +open an issue or bring it to the [Discord](https://discord.gg/enMmUNq8jc). Related: [devsecblueprint#180](https://github.com/devsecblueprint/devsecblueprint/issues/180). --- -## Maintained by +## License and ownership + +MIT — see [`LICENSE.md`](LICENSE.md). Commercial use is permitted, no separate +authorization required. + +The license covers the software in this repository. It grants no rights to DSB +curriculum, walkthroughs, training materials, names, logos, or other branded assets — +see [Trademarks](docs/legal/TRADEMARKS.md). + +Owned and maintained by The DevSec Blueprint. This skill implements DSB knowledge; it +is not the source of that knowledge. Where the skill and the curriculum disagree, the +curriculum wins and the skill is a defect. -The DevSec Blueprint. See [MAINTAINERS.md](MAINTAINERS.md) and join the -[Discord](https://discord.gg/enMmUNq8jc). +[Contributing](docs/CONTRIBUTING.md) · [Testing](docs/TESTING.md) · +[Security](docs/SECURITY.md) · [Code of Conduct](docs/CODE_OF_CONDUCT.md) · +[Maintainers](docs/MAINTAINERS.md) · [Changelog](CHANGELOG.md) diff --git a/SKILL.md b/SKILL.md index f8671e4..ad13372 100644 --- a/SKILL.md +++ b/SKILL.md @@ -15,8 +15,8 @@ description: > # DSB DevSecOps Engineering Maintained by [The DevSec Blueprint](https://github.com/devsecblueprint). -PolyForm Noncommercial 1.0.0 — commercial use requires prior written authorization -from The DevSec Blueprint LLC. +MIT licensed. DSB names, logos, and curriculum content are not covered by that +license. > **DSB defines the required capabilities and engineering outcomes. > The organization determines how those capabilities are implemented.** @@ -238,7 +238,7 @@ Format: **ID — Title** · phase · capability · default enforcement · applic ### DSB-BUILD — build and artifact creation **DSB-BUILD-001 — Builds execute from version-controlled source** -· build · `sbom-generation` · BLOCK · applies when `source.host exists` +· build · `build-traceability` · BLOCK · applies when `source.host exists` **Requirement.** Every deployable artifact must be produced by an automated build executing from a specific, identifiable commit, and must be traceable back to it. **Why.** An artifact that cannot be traced to a commit cannot be reviewed, reproduced, @@ -247,7 +247,7 @@ control; without it, scan results cannot be tied to what shipped. *SSDF PS.1.1, PS.3.1 · SLSA build-L1 · CNCF build-integrity · Curriculum: module-2-1 (What is the Secure SDLC?), module-2-4 (DevSecOps Fundamentals)* **DSB-BUILD-002 — Dependency resolution is controlled and repeatable** -· build · `sbom-generation` · BLOCK · applies when `package_managers exists` +· build · `dependency-resolution-control` · BLOCK · applies when `package_managers exists` **Requirement.** Dependencies must resolve through a controlled source with pinned or locked versions, such that the same commit produces the same dependency set. **Why.** Unpinned resolution means the artifact you scanned and the artifact you ship @@ -264,7 +264,7 @@ rebuilding. It is the difference between "are we exposed?" taking minutes or wee *SSDF PS.3.2 · SLSA build-L2 · CNCF sbom · Curriculum: module-2-4 (DevSecOps Fundamentals)* **DSB-BUILD-004 — Build environments are ephemeral** -· build · `pipeline-configuration-scanning` · WARN · applies when `cicd.runners exists` +· build · `build-environment-isolation` · WARN · applies when `cicd.runners exists` **Requirement.** Builds run in clean, disposable environments rather than long-lived mutable agents carrying state between builds. **Why.** Persistent agents accumulate credentials, caches, and artifacts from other @@ -402,7 +402,7 @@ delivery pressure. Encoded policy applies identically to everyone at 3am. *SSDF PO.1.1 · Curriculum: module-3-7 (IaC Security)* **DSB-IAC-004 — Infrastructure changes are reviewed before application to production** -· deploy · `policy-as-code-enforcement` · BLOCK · applies when `iac.present` and +· deploy · `deployment-gating` · BLOCK · applies when `iac.present` and `deploy.environments` includes production **Requirement.** The planned change set must be produced and reviewed before applying infrastructure changes to production. @@ -413,7 +413,7 @@ the systems needed to fix them. The plan is the last cheap checkpoint. ### DSB-SRC — source control security **DSB-SRC-001 — Deployable branches are protected** -· cross-cutting · `pipeline-configuration-scanning` · BLOCK · applies when `source.host exists` +· cross-cutting · `source-control-hardening` · BLOCK · applies when `source.host exists` **Requirement.** Branches that trigger deployment must prevent direct pushes, force pushes, and unreviewed changes. **Why.** Every downstream control is worthless if code can reach the deployable branch @@ -421,7 +421,7 @@ without passing through the pipeline that enforces them. *SSDF PO.5.2 · OWASP CICD-SEC-1 · Curriculum: module-2-1 (What is the Secure SDLC?)* **DSB-SRC-002 — Changes are reviewed before merge** -· cross-cutting · `pipeline-configuration-scanning` · BLOCK · applies when `source.host exists` +· cross-cutting · `source-control-hardening` · BLOCK · applies when `source.host exists` **Requirement.** Changes to deployable branches require review by someone other than the author. **Why.** Review is the only control that catches intent. Automation catches known @@ -429,7 +429,7 @@ patterns; it does not catch a deliberate or subtle logic change. *SSDF PW.7.1 · SLSA source-L2 · Curriculum: module-2-3 (Secure Coding Overview)* **DSB-SRC-003 — Untrusted contributions cannot access privileged pipeline context** -· cross-cutting · `pipeline-configuration-scanning` · BLOCK · applies when +· cross-cutting · `source-control-hardening` · BLOCK · applies when `source.fork_prs_allowed` **Requirement.** Pipelines triggered by untrusted contributions must not have access to deployment credentials, production secrets, or privileged runners. @@ -441,7 +441,7 @@ only the default configuration. ### DSB-SC — software supply chain **DSB-SC-001 — Dependencies resolve through a controlled source** -· build · `software-composition-analysis` · WARN · applies when `package_managers exists` +· build · `dependency-resolution-control` · WARN · applies when `package_managers exists` **Requirement.** Builds resolve dependencies through an organizationally controlled registry or proxy rather than reaching public registries directly. **Why.** A controlled proxy gives you an inventory of what actually entered your @@ -506,7 +506,7 @@ artifact you ran can differ, which silently invalidates every scan result you ho ### DSB-DEPLOY — deployment and promotion **DSB-DEPLOY-001 — Only artifacts that passed required controls are deployed** -· deploy · `artifact-integrity-verification` · BLOCK · applies when `deploy.targets exists` +· deploy · `deployment-gating` · BLOCK · applies when `deploy.targets exists` **Requirement.** Deployment must be conditional on the applicable Build, Test, and Scan requirements having passed, or on a recorded exception. **Why.** This is the whole point of the phase ordering. Controls that do not gate @@ -515,7 +515,7 @@ deliberate one. *SSDF PW.8.2, RV.1.1 · Curriculum: module-2-1 (What is the Secure SDLC?)* **DSB-DEPLOY-002 — Validated artifacts are promoted, not rebuilt** -· deploy · `artifact-integrity-verification` · BLOCK · applies when +· deploy · `deployment-gating` · BLOCK · applies when `deploy.environments` has more than one environment **Requirement.** Progression between environments promotes the already-validated artifact rather than rebuilding from source per environment. @@ -524,7 +524,7 @@ different artifact than the one running in production. The evidence becomes fict *SLSA build-L2 · SSDF PS.3.1 · Curriculum: module-2-4 (DevSecOps Fundamentals)* **DSB-DEPLOY-003 — Production deployment follows validation in a lower environment** -· deploy · `artifact-integrity-verification` · WARN · applies when `deploy.environments` +· deploy · `deployment-gating` · WARN · applies when `deploy.environments` includes both production and a non-production environment **Requirement.** Artifacts reach production only after being deployed and validated in at least one lower environment. @@ -534,7 +534,7 @@ run safely. *SSDF RV.1.1 · Curriculum: module-2-1 (What is the Secure SDLC?)* **DSB-DEPLOY-004 — Deployment is automated and reproducible** -· deploy · `artifact-integrity-verification` · WARN · applies when `deploy.targets exists` +· deploy · `deployment-gating` · WARN · applies when `deploy.targets exists` **Requirement.** Deployment executes through automation from recorded configuration rather than manual operator steps. **Why.** Manual deployment is unreviewable, unrepeatable, and unattributable. It also @@ -544,7 +544,7 @@ means the credentials involved are held by people rather than scoped to a system ### DSB-ID — workload identity, authentication, authorization **DSB-ID-001 — Pipelines authenticate with short-lived federated identity** -· cross-cutting · `pipeline-configuration-scanning` · WARN · applies when `identity.mechanism exists` +· cross-cutting · `workload-identity-management` · WARN · applies when `identity.mechanism exists` **Requirement.** Where the platform and target support it, pipelines authenticate using short-lived federated workload identity rather than long-lived static credentials. **Why.** A static credential in CI is a permanent, copyable, widely-readable key to @@ -552,7 +552,7 @@ production. Federated identity makes stolen credentials expire on their own. *SSDF PO.5.1 · OWASP CICD-SEC-2 · Curriculum: module-3-2 (IAM Fundamentals)* **DSB-ID-002 — Deployment identity follows least privilege** -· cross-cutting · `pipeline-configuration-scanning` · BLOCK · applies when `deploy.targets exists` +· cross-cutting · `workload-identity-management` · BLOCK · applies when `deploy.targets exists` **Requirement.** Deployment credentials are scoped to the specific environment and resources they deploy, not shared across projects or environments. **Why.** A single over-scoped deployment identity converts a compromise of the least @@ -560,7 +560,7 @@ important pipeline into a compromise of everything it can reach. *SSDF PO.5.1 · OWASP CICD-SEC-2 · Curriculum: module-3-2 (IAM Fundamentals)* **DSB-ID-003 — Secrets are not stored in pipeline definitions or source** -· cross-cutting · `secret-scanning` · BLOCK · applies when `cicd.platform exists` +· cross-cutting · `secrets-management` · BLOCK · applies when `cicd.platform exists` **Requirement.** Credentials are supplied through a secret store or platform secret mechanism, never committed to source or embedded in pipeline definitions. **Why.** Anything in the repository is readable by everyone with repository access, @@ -568,7 +568,7 @@ retained in history, and copied into every clone and fork. *SSDF PO.5.2 · OWASP CICD-SEC-6 · Curriculum: module-3-4 (Secrets Management In The Cloud)* **DSB-ID-004 — Environment credentials are separated** -· cross-cutting · `pipeline-configuration-scanning` · BLOCK · applies when +· cross-cutting · `workload-identity-management` · BLOCK · applies when `deploy.environments` has more than one environment **Requirement.** Each environment uses distinct credentials, and non-production pipelines cannot obtain production credentials. @@ -579,7 +579,7 @@ compromise of production. Non-production is always the least protected. ### DSB-EVD — evidence, logging, observability **DSB-EVD-001 — Security control results are retained as evidence** -· cross-cutting · `pipeline-configuration-scanning` · REPORT · applies when `cicd.platform exists` +· cross-cutting · `pipeline-evidence-retention` · REPORT · applies when `cicd.platform exists` **Requirement.** Scan results and control outcomes are retained and associated with the artifact and commit they describe. **Why.** A control that leaves no record cannot demonstrate it ran, cannot support an @@ -587,7 +587,7 @@ audit, and cannot be compared over time to show whether anything improved. *SSDF PO.4.1, RV.1.1 · Curriculum: module-3-5 (Cloud Logging and Monitoring)* **DSB-EVD-002 — Pipeline execution is auditable** -· cross-cutting · `pipeline-configuration-scanning` · REPORT · applies when `cicd.platform exists` +· cross-cutting · `pipeline-evidence-retention` · REPORT · applies when `cicd.platform exists` **Requirement.** Pipeline runs record what was built, from what source, by whom or what trigger, and what was deployed where. **Why.** During an incident the first questions are what shipped, when, and from what @@ -595,7 +595,7 @@ commit. Without an audit trail those questions take days. *SSDF PO.4.1 · OWASP CICD-SEC-10 · Curriculum: module-3-5 (Cloud Logging and Monitoring)* **DSB-EVD-003 — Findings reach an owning team** -· cross-cutting · `pipeline-configuration-scanning` · REPORT · applies when +· cross-cutting · `security-findings-management` · REPORT · applies when any scanning capability resolves to REUSE or GAP **Requirement.** Security findings are routed to the team that owns remediation, not left only in pipeline output. @@ -606,7 +606,7 @@ reason a technically correct pipeline produces no security improvement. ### DSB-EXC — exceptions and risk acceptance **DSB-EXC-001 — Exceptions are explicit, owned, and time-bound** -· cross-cutting · `policy-as-code-enforcement` · BLOCK · applies when +· cross-cutting · `exception-management` · BLOCK · applies when `policy.exception_process` or any control resolves to BLOCK **Requirement.** Bypassing a BLOCK-level control requires a recorded exception with a named owner, a stated rationale, and an expiry date. @@ -615,7 +615,7 @@ bypasses are not — and without expiry every exception becomes permanent by def *SSDF RV.2.2 · OWASP SAMM governance-policy · Curriculum: module-2-1 (What is the Secure SDLC?)* **DSB-EXC-002 — Suppressions reference an exception** -· cross-cutting · `policy-as-code-enforcement` · WARN · applies when any scanning +· cross-cutting · `exception-management` · WARN · applies when any scanning capability resolves to REUSE **Requirement.** In-code or in-tool suppressions of security findings must reference the exception authorizing them. @@ -642,12 +642,20 @@ DSB requires **capabilities, never vendors**. | Group | Capabilities | |---|---| -| Application and repository | `sast`, `software-composition-analysis`, `secret-scanning`, `license-compliance-analysis` | -| Infrastructure and platform | `iac-scanning`, `kubernetes-configuration-scanning`, `policy-as-code-enforcement`, `pipeline-configuration-scanning` | -| Artifact and supply chain | `container-image-scanning`, `sbom-generation`, `artifact-integrity-verification`, `artifact-signing`, `build-provenance` | +| Application and repository | `sast`, `software-composition-analysis`, `secret-scanning`, `license-compliance-analysis`, `source-control-hardening` | +| Infrastructure and platform | `iac-scanning`, `kubernetes-configuration-scanning`, `policy-as-code-enforcement`, `pipeline-configuration-scanning`, `build-environment-isolation`, `secrets-management`, `workload-identity-management` | +| Artifact and supply chain | `container-image-scanning`, `sbom-generation`, `artifact-integrity-verification`, `artifact-signing`, `build-provenance`, `build-traceability`, `dependency-resolution-control`, `deployment-gating` | | Dynamic and post-deployment | `dast`, `api-security-testing` | +| Governance and evidence | `pipeline-evidence-retention`, `security-findings-management`, `exception-management` | | Engineering | `automated-testing` (not a security control) | +**Capabilities are distinguished by what actually satisfies them, not by what sounds +adjacent.** `secret-scanning` detects committed secrets; `secrets-management` supplies +credentials from a store. `dependency-resolution-control` makes resolution repeatable; +`software-composition-analysis` examines what was resolved. Because resolution keys on +capability, collapsing two of these into one makes a tool that provides only the first +report the second as satisfied. + **Advanced, organization-dependent** — fuzz testing, IAST, specialized compliance scanners, proprietary internal tools, penetration testing workflows. Not baseline. Include only where workload, policy, or organizational requirements make them apply. diff --git a/commands/advise.md b/commands/advise.md new file mode 100644 index 0000000..909b858 --- /dev/null +++ b/commands/advise.md @@ -0,0 +1,25 @@ +--- +description: Advise on where security controls belong in a delivery pipeline, given a stack, constraints, and the tools an organization already owns. +argument-hint: "[your stack, constraints, and existing security tooling]" +--- + +Read `${CLAUDE_PLUGIN_ROOT}/SKILL.md` and apply it in **Advise mode** (§5.1). + +The user's situation: + +$ARGUMENTS + +If that is empty, ask what they are building, on what CI/CD platform, and which +security products the organization already owns — then continue. + +Work the four-step procedure in §2 to a Control Plan, then produce the §5.1 output +structure. Two things carry most of the value here, so do not let them slip: + +- Resolve every capability the organization already owns to **REUSE**, naming the + tool. Recommending a second scanner for a covered capability is a defect, not + thoroughness. +- Emit the **Not applicable** and **undetermined** sections in full. What does not + apply, and what you could not determine, are deliverables — not omissions. + +Do not emit pipeline configuration in this mode. If they want that, use +`/devsecops-engineer:design`. diff --git a/commands/assess.md b/commands/assess.md new file mode 100644 index 0000000..70967d0 --- /dev/null +++ b/commands/assess.md @@ -0,0 +1,23 @@ +--- +description: Assess an existing pipeline, workflow, or repository against DSB requirements and report findings ordered by what would actually be exploited. +argument-hint: "[path to a pipeline file or repository — defaults to this repository]" +--- + +Read `${CLAUDE_PLUGIN_ROOT}/SKILL.md` and apply it in **Review mode** (§5.3). + +What to assess: + +$ARGUMENTS + +If that is empty, find the delivery pipelines in the current repository — CI/CD +configuration under `.github/workflows/`, `Jenkinsfile`, `.gitlab-ci.yml`, +`azure-pipelines.yml`, or equivalent — and assess those. + +Build the Workload Profile from what the pipeline itself reveals, then the Control +Plan, then the Observed Control Set, then diff them. Run the "always check these +explicitly" list in §5.3 — those findings are common, high-impact, and the ones most +often missed. + +Order findings by severity, not by rule number. Lead with what an attacker would +reach first, not with the first control that happens to be absent. Every finding +carries the rule ID, what was observed, why it matters, and concrete remediation. diff --git a/commands/design.md b/commands/design.md new file mode 100644 index 0000000..abe262c --- /dev/null +++ b/commands/design.md @@ -0,0 +1,22 @@ +--- +description: Generate implementation-ready CI/CD pipeline configuration with every security control placed, enforced, and annotated with its DSB rule. +argument-hint: "[what to build, target platform, and where it deploys]" +--- + +Read `${CLAUDE_PLUGIN_ROOT}/SKILL.md` and apply it in **Design / Generate mode** (§5.2). + +What the user wants built: + +$ARGUMENTS + +Respect the completeness gate in §5.2 before generating anything. If the CI/CD +platform, languages, deployment targets, or artifact type are still unresolved, ask +for them and stop there. A plausible-looking pipeline built on guesses is worse than +a question, because it will be run. + +Once the gate is met: build the Control Plan, answer the platform adapter contract +for the target platform, then render. Annotate every security step with the rule it +satisfies, and state which controls block and which only warn. + +If the user is describing an existing pipeline rather than a new one, use +`/devsecops-engineer:assess` instead. diff --git a/CODE_OF_CONDUCT.md b/docs/CODE_OF_CONDUCT.md similarity index 100% rename from CODE_OF_CONDUCT.md rename to docs/CODE_OF_CONDUCT.md diff --git a/CONTRIBUTING.md b/docs/CONTRIBUTING.md similarity index 78% rename from CONTRIBUTING.md rename to docs/CONTRIBUTING.md index 3f2a9e3..f1d9ee7 100644 --- a/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -10,8 +10,8 @@ outcomes, never vendors.** ## Before you start -By contributing you agree to the [Contributor License Agreement](docs/legal/CLA.md). -Contributions are licensed under PolyForm Noncommercial 1.0.0, matching the repository. +By contributing you agree to the [Contributor License Agreement](legal/CLA.md). +Contributions are licensed under the MIT License, matching the repository. For anything larger than a typo, open an issue first. A rule change is a change to engineering guidance that other people will apply to production pipelines — it @@ -71,9 +71,14 @@ Rules live in `SKILL.md` under section 3, grouped by family. · scan · `container-image-scanning` · BLOCK · applies when `artifacts.container`, and not `runtime.serverless` **Requirement.** What must be true. Written as an outcome, not an implementation. **Why.** The engineering reason. This is the part people actually read. -*SSDF PW.4.1 · SLSA build-L2 · OWASP CICD-SEC-4 · Curriculum: module-2-6* +*SSDF PW.4.1 · SLSA build-L2 · OWASP CICD-SEC-4 · Curriculum: module-2-6 (Container Security Overview)* ``` +Every curriculum citation carries the module's real title, exactly as +`references/dsb-curriculum.yaml` spells it. `validate_skill.py` rejects a bare +`module-2-6`, because without the title an agent invents a plausible-sounding one +and sends a learner looking for a module that does not exist under that name. + ### Checklist - [ ] **ID is new and permanent.** Never reuse a retired ID — they appear in audit @@ -83,6 +88,12 @@ Rules live in `SKILL.md` under section 3, grouped by family. `DSB-SC`. - [ ] **Capability exists** in `references/capabilities.yaml`. Add it there first if genuinely new — and check it is not an existing capability under another name. +- [ ] **Capability is what actually satisfies the rule**, not the nearest-sounding + entry in the registry. Resolution keys on capability, so a rule pointed at an + adjacent capability reports itself REUSE the moment the organization declares + any tool providing that other thing. Ask: *would a tool that does only this + capability genuinely satisfy this requirement?* If not, the rule needs a + different capability, or a new one. - [ ] **Applicability references only closed-vocabulary fields** from the Workload Profile in `SKILL.md` §2. - [ ] **Applicability never references `ownership` or `existing_controls`.** Those @@ -111,6 +122,23 @@ answer, the rule is probably too broad. --- +## Commands stay thin + +`commands/` holds the `/devsecops-engineer:*` entry points. Each one selects an +operating mode, passes the user's arguments in, and points at `SKILL.md`. That is all +they may do. + +**No rule logic in a command.** No rule IDs, no enforcement levels, no applicability +conditions, no capability lists, no output tables. The moment a command restates part +of the catalog, there are two catalogs, and the one nobody validates starts drifting — +so `/devsecops-engineer:assess` and a plain "review this pipeline" begin giving +different answers. `tests/test_commands.py` enforces this. + +If a command feels like it needs a rule, the rule belongs in `SKILL.md` where the +validator can see it, and the command should say which mode to run instead. + +--- + ## Changing the methodology The four phases, the twenty baseline principles, and the enforcement model come from @@ -126,7 +154,7 @@ diverge from it. ## Generated files -`references/framework-mappings.md` is generated from the mappings lines in `SKILL.md`. +`docs/framework-mappings.md` is generated from the mappings lines in `SKILL.md`. Do not edit it by hand: ```bash @@ -138,7 +166,7 @@ python tools/generate_mappings.py --check # exit 1 if it is out of date touches mappings will fail CI until it is regenerated. The YAML files under `rules/` are a partial projection of the catalog with a -deliberately limited scope — read [`rules/README.md`](rules/README.md) before adding +deliberately limited scope — read [`rules/README.md`](../rules/README.md) before adding to them. --- diff --git a/MAINTAINERS.md b/docs/MAINTAINERS.md similarity index 89% rename from MAINTAINERS.md rename to docs/MAINTAINERS.md index e4343f0..544c6eb 100644 --- a/MAINTAINERS.md +++ b/docs/MAINTAINERS.md @@ -10,8 +10,8 @@ The DSB DevSecOps Engineering Skill is owned and maintained by | **Owner** | The DevSec Blueprint LLC | | **Organization** | [github.com/devsecblueprint](https://github.com/devsecblueprint) | | **Community** | [Discord](https://discord.gg/enMmUNq8jc) | -| **License** | PolyForm Noncommercial 1.0.0 | -| **Commercial licensing** | [docs/legal/COMMERCIAL-LICENSING.md](docs/legal/COMMERCIAL-LICENSING.md) | +| **License** | MIT | +| **Trademarks and brand** | [docs/legal/TRADEMARKS.md](legal/TRADEMARKS.md) | This skill is an implementation of DSB knowledge and standards. It is not the source of those standards — the DSB curriculum is. Where this skill and the curriculum @@ -48,7 +48,7 @@ change agent behavior has not landed. Test against a live agent before merging. Do not open a public issue for a security problem in this repository or in guidance it produces. Contact the maintainers through the -[DSB Discord](https://discord.gg/enMmUNq8jc) or the commercial licensing contact. +[DSB Discord](https://discord.gg/enMmUNq8jc). ## Related diff --git a/SECURITY.md b/docs/SECURITY.md similarity index 100% rename from SECURITY.md rename to docs/SECURITY.md diff --git a/TESTING.md b/docs/TESTING.md similarity index 90% rename from TESTING.md rename to docs/TESTING.md index 4dd83e5..59c2349 100644 --- a/TESTING.md +++ b/docs/TESTING.md @@ -21,9 +21,12 @@ python -m pytest -q ``` OK: 42 rules across 11 families, no violations -47 passed +64 passed ``` +The rule and family counts are asserted by `tests/test_validate_skill.py`, so they +cannot drift silently. The test count is not — treat it as a floor, not a checksum. + `validate_skill.py` checks rule ID uniqueness, capability-registry membership, curriculum citations resolving against the snapshot, valid phases and enforcement levels, that every rule explains itself, that applicability never references @@ -38,13 +41,24 @@ installed as a single file. A validator nobody has seen fail is not a validator. ```bash -# Break a curriculum citation -sed -i 's/module-2-6, module-3-1/module-9-9/' SKILL.md +# Break a curriculum citation. Note the module id is followed by its title in +# SKILL.md — matching a bare `module-2-6` is what makes this edit land. +python - <<'EOF' +import pathlib +p = pathlib.Path("SKILL.md") +p.write_text(p.read_text().replace("module-2-6 (Container Security Overview)", + "module-9-9 (Container Security Overview)", 1)) +EOF + python tools/validate_skill.py # expect: FAIL, module-9-9 not in snapshot -git checkout SKILL.md 2>/dev/null || sed -i 's/module-9-9/module-2-6, module-3-1/' SKILL.md +git checkout SKILL.md ``` -The test suite covers 14 more failure modes this way — see +**Confirm you saw the failure**, not just that the command ran. An edit that misses +its target leaves the validator passing, which reads exactly like a validator that +works — and is the failure mode this exercise exists to rule out. + +The test suite covers 21 more failure modes this way — see `tests/test_validate_skill.py`. --- diff --git a/references/framework-mappings.md b/docs/framework-mappings.md similarity index 100% rename from references/framework-mappings.md rename to docs/framework-mappings.md diff --git a/docs/legal/CLA.md b/docs/legal/CLA.md index 683e0a5..4d29594 100644 --- a/docs/legal/CLA.md +++ b/docs/legal/CLA.md @@ -22,7 +22,7 @@ You grant DSB and its successors and assigns a worldwide, perpetual, irrevocable This grant includes the right to distribute or license your Contribution: -- under the PolyForm Noncommercial License or another source-available license; +- under the MIT License or another open-source or source-available license; - under a proprietary or commercial license; - as part of a hosted, subscription, membership, training, certification, consulting, or educational offering; - under different or additional terms in the future; and diff --git a/docs/legal/COMMERCIAL-LICENSING.md b/docs/legal/COMMERCIAL-LICENSING.md deleted file mode 100644 index 3f58915..0000000 --- a/docs/legal/COMMERCIAL-LICENSING.md +++ /dev/null @@ -1,52 +0,0 @@ -# Commercial Licensing - -The DevSec Blueprint software distributed with this repository is publicly available under the PolyForm Noncommercial License 1.0.0. That license does **not** grant permission for commercial use. - -## Commercial use requires written approval - -A separate written commercial license from **The DevSec Blueprint LLC** is required before any person or organization may use the software for a commercial purpose. - -Commercial use includes, without limitation: - -- selling, renting, sublicensing, or monetizing the software or a modified version; -- operating the software as part of a paid product, hosted service, managed service, subscription, membership, or software-as-a-service offering; -- using the software to deliver paid courses, bootcamps, consulting, coaching, assessments, certification programs, workforce-development programs, or corporate training; -- incorporating the software into a commercial product or internal commercial platform; -- charging for access to an installation, fork, derivative work, implementation, or hosted environment; -- using the software to generate revenue, reduce the cost of a revenue-generating service, or materially support commercial operations; -- removing DSB branding and reselling or presenting the software as another commercial platform; or -- authorizing another party to perform any of the activities above. - -This list is illustrative and does not replace the terms of the PolyForm Noncommercial License. - -## No implied commercial license - -The following do not grant commercial permission: - -- public access to the source code; -- the ability to fork the repository; -- acceptance of a pull request; -- participation in the DSB community; -- purchase of a DSB membership; -- access to DSB curriculum or walkthroughs; -- attribution to The DevSec Blueprint; or -- prior noncommercial use. - -Commercial permission exists only through a separate written agreement signed by an authorized representative of The DevSec Blueprint LLC. - -## Requesting a commercial license - -To request commercial rights, contact The DevSec Blueprint through its official website or published business contact channel and provide: - -1. the legal name of the person or organization requesting the license; -2. the intended use; -3. whether the software will be hosted, distributed, modified, or embedded; -4. the expected number of users or customers; -5. whether training, consulting, memberships, subscriptions, or certifications are involved; and -6. the requested license term. - -The DevSec Blueprint LLC may approve, reject, limit, price, or condition any commercial license request in its discretion. - -## Separate assets - -A commercial software license does not automatically include rights to DSB trademarks, logos, course materials, walkthroughs, assessments, videos, diagrams, templates, member data, or other proprietary content. Those rights must be expressly granted in writing. diff --git a/docs/legal/TRADEMARKS.md b/docs/legal/TRADEMARKS.md index b047944..ab8aee6 100644 --- a/docs/legal/TRADEMARKS.md +++ b/docs/legal/TRADEMARKS.md @@ -1,6 +1,6 @@ # Trademark and Brand Policy -The software license for this repository does not grant rights to use The DevSec Blueprint's names, logos, marks, or branding. +The software in this repository is MIT licensed. That license covers the code and does **not** grant rights to use The DevSec Blueprint's names, logos, marks, branding, or curriculum content. ## Protected DSB branding diff --git a/examples/advise-terraform-only/README.md b/examples/advise-terraform-only/README.md index 6b6cbcc..87985e7 100644 --- a/examples/advise-terraform-only/README.md +++ b/examples/advise-terraform-only/README.md @@ -49,17 +49,17 @@ secret scanning (DSB-SCAN-003). |---|---|---| | DSB-IAC-001 | `iac-scanning` | BLOCK (default) | | DSB-IAC-003 | `policy-as-code-enforcement` | WARN (default) | -| DSB-IAC-004 | reviewed plan before production apply | BLOCK (default) | -| DSB-TEST-002 | infrastructure validation | BLOCK (default) | +| DSB-IAC-004 | `deployment-gating` | BLOCK (default) | +| DSB-TEST-002 | `automated-testing` | BLOCK (default) | | DSB-SCAN-003 | `secret-scanning` | BLOCK (default) | | DSB-SCAN-005 | `pipeline-configuration-scanning` | WARN (default) | -| DSB-SRC-001 | protected branches | BLOCK (default) | -| DSB-SRC-002 | review before merge | BLOCK (default) | -| DSB-SC-002 | pinned pipeline components | BLOCK (default) | -| DSB-BUILD-001 | traceable to a commit | BLOCK (default) | -| DSB-EVD-001 | evidence retention | REPORT (default) | -| DSB-EVD-002 | auditable execution | REPORT (default) | -| DSB-EXC-003 | no inline suppression | BLOCK (default) | +| DSB-SRC-001 | `source-control-hardening` | BLOCK (default) | +| DSB-SRC-002 | `source-control-hardening` | BLOCK (default) | +| DSB-SC-002 | `pipeline-configuration-scanning` | BLOCK (default) | +| DSB-BUILD-001 | `build-traceability` | BLOCK (default) | +| DSB-EVD-001 | `pipeline-evidence-retention` | REPORT (default) | +| DSB-EVD-002 | `pipeline-evidence-retention` | REPORT (default) | +| DSB-EXC-003 | `pipeline-configuration-scanning` | BLOCK (default) | Secret scanning matters more here than in a typical application repository, not less — Terraform repositories accumulate provider credentials, connection strings, and diff --git a/examples/generate-github-actions/README.md b/examples/generate-github-actions/README.md index 42535aa..7592eeb 100644 --- a/examples/generate-github-actions/README.md +++ b/examples/generate-github-actions/README.md @@ -27,12 +27,12 @@ Nothing here is inferred. Every field was stated by the requester. | Rule | Capability | Resolution | Phase | Enforcement | |---|---|---|---|---| -| DSB-BUILD-001 | traceable build | GAP → implemented | build | BLOCK (default) | -| DSB-BUILD-002 | pinned dependencies | GAP → `npm ci` | build | BLOCK (default) | +| DSB-BUILD-001 | `build-traceability` | GAP → implemented | build | BLOCK (default) | +| DSB-BUILD-002 | `dependency-resolution-control` | GAP → `npm ci` | build | BLOCK (default) | | DSB-BUILD-003 | `sbom-generation` | GAP → CycloneDX | build | WARN (default) | -| DSB-BUILD-004 | ephemeral build env | SATISFIED by hosted runners | build | WARN (default) | +| DSB-BUILD-004 | `build-environment-isolation` | SATISFIED by hosted runners | build | WARN (default) | | DSB-TEST-001 | `automated-testing` | GAP → unit + integration | test | BLOCK (default) | -| DSB-TEST-002 | infra validation | GAP → `terraform validate` | test | BLOCK (default) | +| DSB-TEST-002 | `automated-testing` | GAP → `terraform validate` | test | BLOCK (default) | | DSB-SCAN-001 | `sast` | GAP → SAST scanner | scan | BLOCK (default) | | DSB-SCAN-002 | `software-composition-analysis` | GAP → dependency audit | scan | BLOCK (default) | | DSB-SCAN-003 | `secret-scanning` | GAP → history scan | scan | BLOCK (default) | @@ -40,18 +40,18 @@ Nothing here is inferred. Every field was stated by the requester. | DSB-SCAN-005 | `pipeline-configuration-scanning` | GAP → workflow assertions | scan | WARN (default) | | DSB-SCAN-006 | `dast` | GAP → staging DAST | post-deploy | WARN (default) | | DSB-IAC-001 | `iac-scanning` | GAP → Terraform config scan | scan | BLOCK (default) | -| DSB-SC-002 | pinned pipeline components | GAP → SHA-pin assertion | cross-cutting | BLOCK (default) | -| DSB-ART-001 | controlled registry | GAP → ECR push | deploy | BLOCK (default) | +| DSB-SC-002 | `pipeline-configuration-scanning` | GAP → SHA-pin assertion | cross-cutting | BLOCK (default) | +| DSB-ART-001 | `artifact-integrity-verification` | GAP → ECR push | deploy | BLOCK (default) | | DSB-ART-003 | `build-provenance` | GAP → attestation | build | WARN (default) | -| DSB-ART-004 | deploy by immutable id | GAP → deploy by digest | deploy | BLOCK (default) | -| DSB-DEPLOY-001 | gated deployment | GAP → job `needs` graph | deploy | BLOCK (default) | -| DSB-DEPLOY-002 | promote, don't rebuild | GAP → one build, two deploys | deploy | BLOCK (default) | -| DSB-DEPLOY-003 | staging before production | GAP → job ordering | deploy | WARN (default) | -| DSB-ID-001 | federated identity | GAP → OIDC role assumption | cross-cutting | WARN (default) | -| DSB-ID-002 | least privilege | GAP → scoped `permissions` | cross-cutting | BLOCK (default) | -| DSB-ID-004 | environment separation | GAP → per-environment roles | cross-cutting | BLOCK (default) | -| DSB-EVD-001 | evidence retention | GAP → 90-day artifacts | cross-cutting | REPORT (default) | -| DSB-EXC-003 | no inline suppression | GAP → suppression assertion | cross-cutting | BLOCK (default) | +| DSB-ART-004 | `artifact-integrity-verification` | GAP → deploy by digest | deploy | BLOCK (default) | +| DSB-DEPLOY-001 | `deployment-gating` | GAP → job `needs` graph | deploy | BLOCK (default) | +| DSB-DEPLOY-002 | `deployment-gating` | GAP → one build, two deploys | deploy | BLOCK (default) | +| DSB-DEPLOY-003 | `deployment-gating` | GAP → job ordering | deploy | WARN (default) | +| DSB-ID-001 | `workload-identity-management` | GAP → OIDC role assumption | cross-cutting | WARN (default) | +| DSB-ID-002 | `workload-identity-management` | GAP → scoped `permissions` | cross-cutting | BLOCK (default) | +| DSB-ID-004 | `workload-identity-management` | GAP → per-environment roles | cross-cutting | BLOCK (default) | +| DSB-EVD-001 | `pipeline-evidence-retention` | GAP → 90-day artifacts | cross-cutting | REPORT (default) | +| DSB-EXC-003 | `pipeline-configuration-scanning` | GAP → suppression assertion | cross-cutting | BLOCK (default) | **Not applicable:** @@ -88,12 +88,30 @@ is not authorization (DSB-SCAN-006). A static key in CI is a permanent, copyable credential to production (DSB-ID-001, DSB-ID-004). +**Tests gate the scans.** The three scan jobs run in parallel with each other but +all depend on `test`. Scanning alongside testing would spend scanner capacity and +wall-clock time on an artifact a failing unit test already disqualified +(DSB-TEST-001). + **The pipeline audits itself.** Two assertions fail the build if a third-party action is unpinned (DSB-SC-002) or if any security step has been neutralized with `|| true` or `continue-on-error` (DSB-EXC-003). +Both hold their patterns in `env` and bracket-escape them, so neither assertion +matches its own source. A self-matching check fails on a clean repository, which +looks identical to a real finding and is how these checks end up deleted. The +suppression check also allows a line annotated `# DSB-WARN: `, because +`continue-on-error` is how this platform expresses a WARN-level control (§5.2) — +what DSB-EXC-003 forbids is the undeclared kind. + ## Before using this +- **Set the repository variables it reads.** `ECR_REGISTRY` (the registry host, + which is what makes `docker push` reach the controlled registry rather than the + public default), `AWS_ECR_PUBLISH_ROLE`, `AWS_DEPLOY_ROLE_STAGING`, + `AWS_DEPLOY_ROLE_PRODUCTION`, and `STAGING_URL`. The publish role is deliberately + separate from the deployment roles — the build job has no reason to hold either + (DSB-ID-002). - **Pin the scanner images by digest.** This example pins them by version tag for readability. Production use should pin by digest. - **Verify the action SHAs.** Those shown correspond to the tagged versions in diff --git a/examples/generate-github-actions/delivery.yml b/examples/generate-github-actions/delivery.yml index 16970b4..c776285 100644 --- a/examples/generate-github-actions/delivery.yml +++ b/examples/generate-github-actions/delivery.yml @@ -26,6 +26,10 @@ permissions: env: IMAGE_NAME: payments-api AWS_REGION: us-east-1 + ECS_CLUSTER: payments + # DSB-ART-001: the controlled registry of record. Pushing a bare image name + # would publish to the public default registry instead. + ECR_REGISTRY: ${{ vars.ECR_REGISTRY }} jobs: # ─────────────────────────────────────────────────────────────────────── @@ -79,10 +83,15 @@ jobs: - run: npm run test:integration # integration # ─────────────────────────────────────────────────────────────────────── - # SCAN — source-layer controls. These need only the repository, so they run - # in parallel with test rather than waiting on it. + # SCAN — source-layer controls. + # + # DSB-TEST-001 (BLOCK) puts Test before Scan: failing fast on functional + # defects avoids spending scanner capacity and wall-clock time on an artifact + # that was never going to ship. The three scan jobs are parallel with each + # other, but all of them gate on test. # ─────────────────────────────────────────────────────────────────────── scan-source: + needs: test runs-on: ubuntu-latest permissions: contents: read @@ -123,6 +132,7 @@ jobs: # DSB-IAC-001 (BLOCK) and DSB-TEST-002 (BLOCK): validate before apply. # ─────────────────────────────────────────────────────────────────────── scan-infrastructure: + needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -142,23 +152,39 @@ jobs: # DSB-SCAN-005 (WARN, default) and DSB-SC-002 (BLOCK, default). # ─────────────────────────────────────────────────────────────────────── scan-pipeline: + needs: test runs-on: ubuntu-latest steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 # DSB-SC-002: every third-party action must be pinned to a commit SHA. # A mutable tag lets a third party change what runs with our credentials. + # + # The patterns below are bracket-escaped and held in env rather than + # written inline. A self-describing assertion that matches its own source + # fails on a clean repository, which is indistinguishable from a real + # finding and trains people to delete the check. - name: Assert actions are SHA-pinned + env: + UNPINNED: 'uses:[[:space:]]+[^@[:space:]]+@(v[0-9]|main|master)' run: | - if grep -rEn 'uses: [^@]+@(v[0-9]|main|master)' .github/workflows/; then + if grep -rEn "$UNPINNED" .github/workflows/; then echo "::error::unpinned action reference (DSB-SC-002)" exit 1 fi # DSB-EXC-003: controls must not be neutralized inline. + # + # A WARN-level control legitimately uses `continue-on-error` — that is how + # this platform expresses WARN. The difference between WARN and a + # suppressed BLOCK is whether it was a decision, so a deliberate one is + # annotated `# DSB-WARN: ` on the same line and anything else + # fails. - name: Assert no suppressed security steps + env: + SUPPRESSION: '[|][|][[:space:]]+true|continue-on-error:[[:space:]]*true' run: | - if grep -rEn '\|\| true|continue-on-error: true' .github/workflows/; then + if grep -rEn "$SUPPRESSION" .github/workflows/ | grep -v 'DSB-WARN:'; then echo "::error::security step suppression detected (DSB-EXC-003)" exit 1 fi @@ -178,6 +204,7 @@ jobs: packages: write attestations: write # DSB-ART-003: build provenance outputs: + image: ${{ steps.push.outputs.image }} digest: ${{ steps.push.outputs.digest }} steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -186,34 +213,54 @@ jobs: with: name: build-output - - run: docker build -t "$IMAGE_NAME:${{ needs.build.outputs.version }}" . + # One tagged reference, derived once, used by build/scan/push. Repeating + # the expression at each step is how a pipeline ends up scanning one image + # and pushing another. + - name: Resolve the image reference + run: | + echo "IMAGE_REF=$ECR_REGISTRY/$IMAGE_NAME:${{ needs.build.outputs.version }}" \ + >> "$GITHUB_ENV" + + - run: docker build -t "$IMAGE_REF" . - # DSB-SCAN-004: scan the local image before it is pushed. + # DSB-SCAN-004: scan the local image before it is pushed. Everything + # below this step runs only if the scan passed. - name: Container image scan run: | docker run --rm -v /var/run/docker.sock:/var/run/docker.sock \ aquasec/trivy:0.58.1 image \ --exit-code 1 --severity HIGH,CRITICAL --ignore-unfixed \ - "$IMAGE_NAME:${{ needs.build.outputs.version }}" + "$IMAGE_REF" # DSB-ID-001 (WARN, default): short-lived federated identity. No # long-lived AWS keys are stored anywhere in this repository. + # + # DSB-ID-002 (BLOCK, default): this is a publish-only role. The build job + # never holds a deployment role — those are assumed by the deploy jobs, + # scoped per environment. - uses: aws-actions/configure-aws-credentials@e3dd6a429d7300a6a4c196c26e071d42e0343502 # v4.0.2 with: - role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_STAGING }} + role-to-assume: ${{ vars.AWS_ECR_PUBLISH_ROLE }} aws-region: ${{ env.AWS_REGION }} - # DSB-ART-001 (BLOCK, default): publish to a controlled registry. + # DSB-ART-001 (BLOCK, default): publish to the controlled registry. - id: push run: | - docker push "$IMAGE_NAME:${{ needs.build.outputs.version }}" - echo "digest=$(docker inspect --format='{{index .RepoDigests 0}}' \ - "$IMAGE_NAME:${{ needs.build.outputs.version }}")" >> "$GITHUB_OUTPUT" + aws ecr get-login-password --region "$AWS_REGION" \ + | docker login --username AWS --password-stdin "$ECR_REGISTRY" + + docker push "$IMAGE_REF" + + # DSB-ART-004: capture the immutable identity at push time. Everything + # downstream references this, never the tag. + REF=$(docker inspect --format='{{index .RepoDigests 0}}' "$IMAGE_REF") + echo "image=$REF" >> "$GITHUB_OUTPUT" + echo "digest=${REF#*@}" >> "$GITHUB_OUTPUT" # DSB-ART-003 (WARN, default): attestable provenance. - uses: actions/attest-build-provenance@ef244123eb79f2f7a7e75d99086184180e6d0018 # v1.4.4 with: - subject-name: ${{ env.IMAGE_NAME }} + subject-name: ${{ env.ECR_REGISTRY }}/${{ env.IMAGE_NAME }} subject-digest: ${{ steps.push.outputs.digest }} # ─────────────────────────────────────────────────────────────────────── @@ -234,10 +281,30 @@ jobs: role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_STAGING }} aws-region: ${{ env.AWS_REGION }} + # DSB-ART-004 (BLOCK, default): the service is pinned to the digest the + # scan passed against. Registering a revision that named the tag would + # let the running artifact drift from the scanned one. - name: Deploy by digest + env: + SERVICE: payments-api-staging + IMAGE: ${{ needs.image.outputs.image }} run: | - aws ecs deploy --service payments-api-staging \ - --image "${{ needs.image.outputs.digest }}" + aws ecs describe-task-definition --task-definition "$SERVICE" \ + --query taskDefinition --output json \ + | jq --arg img "$IMAGE" \ + '.containerDefinitions[0].image = $img + | del(.taskDefinitionArn, .revision, .status, .registeredAt, + .registeredBy, .requiresAttributes, .compatibilities)' \ + > task-definition.json + + REVISION=$(aws ecs register-task-definition \ + --cli-input-json file://task-definition.json \ + --query taskDefinition.taskDefinitionArn --output text) + + aws ecs update-service --cluster "$ECS_CLUSTER" \ + --service "$SERVICE" --task-definition "$REVISION" + + aws ecs wait services-stable --cluster "$ECS_CLUSTER" --services "$SERVICE" # ─────────────────────────────────────────────────────────────────────── # POST-DEPLOYMENT SCAN — controls that need a running workload. @@ -273,7 +340,26 @@ jobs: role-to-assume: ${{ vars.AWS_DEPLOY_ROLE_PRODUCTION }} aws-region: ${{ env.AWS_REGION }} + # The same digest staging validated. No rebuild, no re-tag, no second + # scan of a different artifact (DSB-DEPLOY-002, DSB-ART-004). - name: Promote the validated artifact + env: + SERVICE: payments-api-production + IMAGE: ${{ needs.image.outputs.image }} run: | - aws ecs deploy --service payments-api-production \ - --image "${{ needs.image.outputs.digest }}" + aws ecs describe-task-definition --task-definition "$SERVICE" \ + --query taskDefinition --output json \ + | jq --arg img "$IMAGE" \ + '.containerDefinitions[0].image = $img + | del(.taskDefinitionArn, .revision, .status, .registeredAt, + .registeredBy, .requiresAttributes, .compatibilities)' \ + > task-definition.json + + REVISION=$(aws ecs register-task-definition \ + --cli-input-json file://task-definition.json \ + --query taskDefinition.taskDefinitionArn --output text) + + aws ecs update-service --cluster "$ECS_CLUSTER" \ + --service "$SERVICE" --task-definition "$REVISION" + + aws ecs wait services-stable --cluster "$ECS_CLUSTER" --services "$SERVICE" diff --git a/examples/generate-jenkins/Jenkinsfile b/examples/generate-jenkins/Jenkinsfile index 7413fac..781382a 100644 --- a/examples/generate-jenkins/Jenkinsfile +++ b/examples/generate-jenkins/Jenkinsfile @@ -12,7 +12,12 @@ // Per-repository Jenkinsfiles should stay thin and call a shared library, so a // security policy change does not require a pull request against every service. -@Library('platform-pipeline@v3') _ +// DSB-SC-002 (BLOCK, default): a shared library is a third-party pipeline +// component and is pinned to an immutable commit, not to `@v3`. A moving tag +// means whoever can push to the library changes what executes here, with these +// credentials, with no change on this side. Jenkins accepts a commit hash as +// the library version wherever it accepts a tag. +@Library('platform-pipeline@4f9c1d2a8b3e6570c1d4a9f2e8b7c3d05a6e1f94') _ // v3.2.1 pipeline { // DSB-BUILD-004 (WARN, default): ephemeral agents. Static nodes accumulate diff --git a/examples/generate-jenkins/README.md b/examples/generate-jenkins/README.md index a8a7e52..d983f4d 100644 --- a/examples/generate-jenkins/README.md +++ b/examples/generate-jenkins/README.md @@ -46,7 +46,7 @@ policy: { enforcement_posture: strict, dast_prod_authorized: false } | `software-composition-analysis` | **REUSE — Black Duck** | DSB-SCAN-002 | | `container-image-scanning` | **REUSE — Prisma Cloud** | DSB-SCAN-004 | | `artifact-integrity-verification` | **REUSE — Artifactory** | DSB-ART-001, DSB-ART-004 | -| controlled dependency source | **REUSE — Artifactory virtual repo** | DSB-SC-001 | +| `dependency-resolution-control` | **REUSE — Artifactory virtual repo** | DSB-SC-001 | | `secret-scanning` | GAP or DELEGATED — see below | DSB-SCAN-003 | | `sbom-generation` | GAP | DSB-BUILD-003 | | `dast` | GAP | DSB-SCAN-006 | diff --git a/references/capabilities.yaml b/references/capabilities.yaml index c504377..20b2bfc 100644 --- a/references/capabilities.yaml +++ b/references/capabilities.yaml @@ -22,6 +22,13 @@ capabilities: summary: Analysis of dependency licenses against organizational policy. typical_phase: scan + - id: source-control-hardening + group: application-and-repository + summary: > + Branch protection, review enforcement, and trigger isolation on the + repositories that feed the pipeline. + typical_phase: cross-cutting + - id: iac-scanning group: infrastructure-and-platform summary: Static analysis of infrastructure-as-code for insecure configuration. @@ -42,6 +49,25 @@ capabilities: summary: Analysis of CI/CD configuration for insecure pipeline practices. typical_phase: scan + - id: build-environment-isolation + group: infrastructure-and-platform + summary: Provision of clean, disposable build environments per execution. + typical_phase: build + + - id: secrets-management + group: infrastructure-and-platform + summary: > + Supply of credentials to the pipeline from a secret store rather than from + source or pipeline definitions. Distinct from detecting leaked secrets. + typical_phase: cross-cutting + + - id: workload-identity-management + group: infrastructure-and-platform + summary: > + Issuance and scoping of the identities a pipeline authenticates with — + federation, least privilege, and per-environment separation. + typical_phase: cross-cutting + - id: container-image-scanning group: artifact-and-supply-chain summary: Vulnerability scanning of container images including base OS packages. @@ -67,6 +93,27 @@ capabilities: summary: Attestation describing how, where, and from what an artifact was built. typical_phase: build + - id: build-traceability + group: artifact-and-supply-chain + summary: > + Binding of every deployable artifact to the specific commit and automated + build that produced it. + typical_phase: build + + - id: dependency-resolution-control + group: artifact-and-supply-chain + summary: > + Repeatable dependency resolution through a controlled source with pinned or + locked versions. Distinct from analysing what those dependencies contain. + typical_phase: build + + - id: deployment-gating + group: artifact-and-supply-chain + summary: > + Conditioning deployment and environment promotion on the applicable controls + having passed, and on promoting rather than rebuilding. + typical_phase: deploy + - id: dast group: dynamic-and-post-deployment summary: Dynamic security testing against a running instance of the workload. @@ -77,6 +124,25 @@ capabilities: summary: Security testing of API surfaces against a running instance. typical_phase: post-deploy + - id: pipeline-evidence-retention + group: governance-and-evidence + summary: > + Retention of control results and execution records, associated with the + artifact and commit they describe. + typical_phase: cross-cutting + + - id: security-findings-management + group: governance-and-evidence + summary: Routing of security findings to the team that owns remediation. + typical_phase: cross-cutting + + - id: exception-management + group: governance-and-evidence + summary: > + Recording, ownership, and expiry of authorized bypasses of a security + control, and the suppressions that reference them. + typical_phase: cross-cutting + - id: automated-testing group: engineering summary: Automated functional validation of the built artifact. Not a security control. diff --git a/references/dsb-curriculum.yaml b/references/dsb-curriculum.yaml index 2aef6fa..640744c 100644 --- a/references/dsb-curriculum.yaml +++ b/references/dsb-curriculum.yaml @@ -1,6 +1,6 @@ # Snapshot of The DevSec Blueprint curriculum index. # Source of truth: frontend/lib/curriculum-data.ts in devsecblueprint/devsecblueprint. -# Refresh procedure is documented in CONTRIBUTING.md. +# Refresh procedure is documented in docs/CONTRIBUTING.md. # # Module names are quoted: several contain '?', which YAML flow mappings reject. source: https://github.com/devsecblueprint/devsecblueprint/blob/main/frontend/lib/curriculum-data.ts diff --git a/rules/README.md b/rules/README.md index 3a1d241..b2fba5d 100644 --- a/rules/README.md +++ b/rules/README.md @@ -42,7 +42,7 @@ apply based on how *other* controls resolve — whether anything came back `REUS `GAP`, or `BLOCK`. That is deliberately not expressible here. Applicability answers "does this risk exist for this workload?"; resolution answers "who satisfies it?" Mixing them destroys the `DELEGATED` outcome, and both -[`CONTRIBUTING.md`](../CONTRIBUTING.md) and the validator forbid it. These rules +[`CONTRIBUTING.md`](../docs/CONTRIBUTING.md) and the validator forbid it. These rules need a second evaluation pass, not a predicate. ## Adding a rule here diff --git a/rules/build/DSB-BUILD-001.yaml b/rules/build/DSB-BUILD-001.yaml index 4031acd..979ada2 100644 --- a/rules/build/DSB-BUILD-001.yaml +++ b/rules/build/DSB-BUILD-001.yaml @@ -2,7 +2,7 @@ id: DSB-BUILD-001 title: Builds execute from version-controlled source family: BUILD phase: build -capability: sbom-generation +capability: build-traceability requirement: > Every deployable artifact must be produced by an automated build that executes from a specific, identifiable commit in version control, and the resulting @@ -22,7 +22,7 @@ enforcement: default_level: BLOCK automatable: true tooling: - examples: [Syft, CycloneDX, native CI build metadata] + examples: [native CI build metadata, Artifactory build-info, git commit pinning] prefer_existing_tool: true dsb_curriculum: [module-2-1, module-2-4] status: active diff --git a/schema/workload-profile.schema.json b/schema/workload-profile.schema.json index aac4ed4..b68d478 100644 --- a/schema/workload-profile.schema.json +++ b/schema/workload-profile.schema.json @@ -12,6 +12,12 @@ { "const": "unknown" } ] }, + "stringlist": { + "oneOf": [ + { "type": "array", "items": { "type": "string" } }, + { "const": "unknown" } + ] + }, "control": { "type": "object", "additionalProperties": false, @@ -33,18 +39,13 @@ "properties": { "host": { "type": "string", "enum": ["github", "gitlab", "bitbucket", "azure-repos", "other", "unknown"] }, "protected_branches": { "$ref": "#/$defs/tristate" }, - "review_required": { "$ref": "#/$defs/tristate" } + "review_required": { "$ref": "#/$defs/tristate" }, + "fork_prs_allowed": { "$ref": "#/$defs/tristate" } } }, - "languages": { - "type": "array", - "items": { "type": "string" } - }, - "package_managers": { - "type": "array", - "items": { "type": "string" } - }, + "languages": { "$ref": "#/$defs/stringlist" }, + "package_managers": { "$ref": "#/$defs/stringlist" }, "artifacts": { "type": "object", @@ -63,7 +64,7 @@ "additionalProperties": false, "properties": { "present": { "$ref": "#/$defs/tristate" }, - "tools": { "type": "array", "items": { "type": "string" } } + "tools": { "$ref": "#/$defs/stringlist" } } }, @@ -82,8 +83,8 @@ "type": "object", "additionalProperties": false, "properties": { - "targets": { "type": "array", "items": { "type": "string" } }, - "environments": { "type": "array", "items": { "type": "string" } }, + "targets": { "$ref": "#/$defs/stringlist" }, + "environments": { "$ref": "#/$defs/stringlist" }, "promotion_model": { "type": "string", "enum": ["rebuild", "promote", "unknown"] } } }, @@ -93,7 +94,8 @@ "additionalProperties": false, "properties": { "platform": { "type": "string", "enum": ["github-actions", "jenkins", "gitlab-ci", "azure-devops", "other", "unknown"] }, - "runners": { "type": "string", "enum": ["hosted", "self-hosted", "mixed", "unknown"] } + "runners": { "type": "string", "enum": ["hosted", "self-hosted", "mixed", "unknown"] }, + "shared_library": { "$ref": "#/$defs/tristate" } } }, @@ -105,6 +107,15 @@ } }, + "registry": { + "type": "object", + "additionalProperties": false, + "properties": { + "present": { "$ref": "#/$defs/tristate" }, + "controlled_proxy": { "$ref": "#/$defs/tristate" } + } + }, + "testing": { "type": "object", "additionalProperties": false, @@ -140,8 +151,10 @@ "properties": { "enforcement_posture": { "type": "string", "enum": ["strict", "balanced", "advisory", "unknown"] }, "exception_process": { "$ref": "#/$defs/tristate" }, - "compliance_regimes": { "type": "array", "items": { "type": "string" } }, - "dast_prod_authorized": { "$ref": "#/$defs/tristate" } + "compliance_regimes": { "$ref": "#/$defs/stringlist" }, + "dast_prod_authorized": { "$ref": "#/$defs/tristate" }, + "signing_required": { "$ref": "#/$defs/tristate" }, + "provenance_required": { "$ref": "#/$defs/tristate" } } } } diff --git a/tests/test_commands.py b/tests/test_commands.py new file mode 100644 index 0000000..fb3a268 --- /dev/null +++ b/tests/test_commands.py @@ -0,0 +1,98 @@ +import json +import pathlib +import re + +import pytest +import yaml + +ROOT = pathlib.Path(__file__).resolve().parents[1] +COMMANDS = ROOT / "commands" +MANIFEST = ROOT / ".claude-plugin" / "plugin.json" +CHANGELOG = ROOT / "CHANGELOG.md" + +EXPECTED = {"advise", "design", "assess"} + + +def command_files(): + return sorted(COMMANDS.glob("*.md")) + + +def frontmatter(path): + match = re.match(r"^---\n(.*?)\n---\n", path.read_text(), re.DOTALL) + assert match, f"{path.name}: missing YAML frontmatter" + return yaml.safe_load(match.group(1)) + + +def body(path): + return re.sub(r"^---\n.*?\n---\n", "", path.read_text(), flags=re.DOTALL) + + +@pytest.fixture(scope="module") +def manifest(): + return json.loads(MANIFEST.read_text()) + + +def test_the_three_entry_points_exist(): + assert {p.stem for p in command_files()} == EXPECTED + + +def test_plugin_name_produces_the_documented_namespace(manifest): + """Commands are invoked as /:. The name is the namespace, + so renaming the plugin silently renames every documented command.""" + assert manifest["name"] == "devsecops-engineer" + + +def test_manifest_declares_release_metadata(manifest): + assert re.fullmatch(r"\d+\.\d+\.\d+", manifest["version"]), manifest["version"] + assert manifest["license"] == "MIT" + assert manifest["description"] + + +def test_manifest_version_matches_the_changelog(manifest): + """A release where the manifest and the changelog disagree is a release where + nobody can tell which one shipped.""" + versions = re.findall(r"^## \[(\d+\.\d+\.\d+)\]", CHANGELOG.read_text(), re.MULTILINE) + assert versions, "CHANGELOG.md has no released version heading" + assert versions[0] == manifest["version"] + + +@pytest.mark.parametrize("path", command_files(), ids=lambda p: p.stem) +def test_command_declares_a_description(path): + """The description is what a user reads in the slash-command menu.""" + assert len(frontmatter(path).get("description") or "") >= 40 + + +@pytest.mark.parametrize("path", command_files(), ids=lambda p: p.stem) +def test_command_loads_the_skill_rather_than_restating_it(path): + assert "${CLAUDE_PLUGIN_ROOT}/SKILL.md" in body(path), ( + f"{path.name}: must point at SKILL.md, which is the single catalog" + ) + + +@pytest.mark.parametrize("path", command_files(), ids=lambda p: p.stem) +def test_command_takes_the_users_arguments(path): + assert "$ARGUMENTS" in body(path), f"{path.name}: never receives what the user typed" + + +@pytest.mark.parametrize("path", command_files(), ids=lambda p: p.stem) +def test_command_contains_no_rule_logic(path): + """Commands select a mode. They do not carry the catalog. + + A command that names rules or enforcement levels is a second copy of the + catalog that no validator checks, so it drifts — and then the command and a + plain-language request give different answers. + """ + text = path.read_text() + + rule_ids = re.findall(r"DSB-[A-Z]+-\d{3}", text) + assert not rule_ids, f"{path.name}: restates rules {sorted(set(rule_ids))}" + + levels = re.findall(r"\b(BLOCK|WARN|REPORT)\b", text) + assert not levels, f"{path.name}: restates enforcement levels {sorted(set(levels))}" + + +@pytest.mark.parametrize("path", command_files(), ids=lambda p: p.stem) +def test_command_stays_short(path): + """A thin wrapper that grew past a screen is no longer a wrapper.""" + lines = [ln for ln in body(path).splitlines() if ln.strip()] + assert len(lines) <= 30, f"{path.name}: {len(lines)} lines — move content into SKILL.md" diff --git a/tests/test_curriculum.py b/tests/test_curriculum.py index f342659..dda430b 100644 --- a/tests/test_curriculum.py +++ b/tests/test_curriculum.py @@ -1,3 +1,4 @@ +import datetime import pathlib import re @@ -23,6 +24,13 @@ def test_module_ids_are_unique_and_well_formed(): def test_snapshot_records_its_source_and_date(): + """Asserts the shape, not the value. + + docs/CONTRIBUTING.md tells a maintainer to bump `snapshot_date` when refreshing + the curriculum. Pinning the literal here would turn following the documented + procedure into a CI failure, so this checks the field is present and is a + real date instead. + """ data = load() assert data["source"] - assert data["snapshot_date"] == "2026-08-12" + datetime.date.fromisoformat(str(data["snapshot_date"])) diff --git a/tests/test_examples.py b/tests/test_examples.py index 9c89429..111202d 100644 --- a/tests/test_examples.py +++ b/tests/test_examples.py @@ -37,12 +37,34 @@ def test_github_actions_example_is_valid_yaml(): def test_github_actions_example_pins_third_party_actions(): - """The examples must obey DSB-SC-002, which they teach.""" + """The examples must obey DSB-SC-002, which they teach. + + Deliberately not anchored to end-of-line: every pinned `uses:` in the + example carries a trailing `# v4.2.2` comment, so an end anchor would let + `uses: foo/bar@v1 # whatever` through — the exact thing this checks for. + """ workflow = (EXAMPLES / "generate-github-actions" / "delivery.yml").read_text() - unpinned = re.findall(r"uses:\s*(\S+@(?:v[\d.]+|main|master))\s*$", workflow, re.MULTILINE) + unpinned = re.findall( + r"uses:\s*([^@\s]+@(?:v[\d.]+|main|master))\b", workflow + ) assert not unpinned, f"unpinned action references: {unpinned}" +def test_jenkins_example_pins_its_shared_library(): + """DSB-SC-002 covers shared libraries too, not only GitHub Actions. + + A Jenkins shared library executes inside the pipeline with its credentials. + `@Library('x@v3')` is a mutable reference and is the same defect as an + unpinned action. + """ + jenkinsfile = (EXAMPLES / "generate-jenkins" / "Jenkinsfile").read_text() + for library, version in re.findall(r"@Library\(['\"]([^@'\"]+)@([^'\"]+)['\"]", jenkinsfile): + assert re.fullmatch(r"[0-9a-f]{40}", version), ( + f"shared library {library!r} pinned to mutable reference {version!r} " + "— DSB-SC-002 requires an immutable commit" + ) + + def test_no_example_suppresses_a_security_step(): """The examples must obey DSB-EXC-003, which they teach. diff --git a/tests/test_mappings.py b/tests/test_mappings.py index 2bf94d0..0aa1cb9 100644 --- a/tests/test_mappings.py +++ b/tests/test_mappings.py @@ -17,7 +17,7 @@ def test_generated_file_is_committed_and_current(): """ assert OUTPUT.exists(), f"{OUTPUT} is missing — run tools/generate_mappings.py" assert OUTPUT.read_text() == render(rules()), ( - "references/framework-mappings.md is out of date — " + "docs/framework-mappings.md is out of date — " "run python tools/generate_mappings.py" ) diff --git a/tests/test_profile_schema.py b/tests/test_profile_schema.py index 70d03ac..18d617d 100644 --- a/tests/test_profile_schema.py +++ b/tests/test_profile_schema.py @@ -1,5 +1,6 @@ import json import pathlib +import re import jsonschema import pytest @@ -7,6 +8,22 @@ ROOT = pathlib.Path(__file__).resolve().parents[1] SCHEMA_PATH = ROOT / "schema" / "workload-profile.schema.json" +SKILL_PATH = ROOT / "SKILL.md" +EXAMPLES = ROOT / "examples" + +PROFILE_BLOCK = re.compile( + r"^## Workload profile\s*\n+```yaml\n(.*?)\n```", re.MULTILINE | re.DOTALL +) + + +def example_profiles(): + """The workload profiles published in the example READMEs.""" + found = [] + for path in sorted(EXAMPLES.rglob("README.md")): + match = PROFILE_BLOCK.search(path.read_text()) + if match: + found.append((str(path.relative_to(ROOT)), yaml.safe_load(match.group(1)))) + return found @pytest.fixture(scope="module") @@ -40,6 +57,49 @@ def test_unknown_top_level_field_is_rejected(schema): jsonschema.validate(profile, schema) +def test_every_documented_profile_field_is_in_the_schema(schema): + """SKILL.md §2 publishes the closed vocabulary; the schema enforces it. + + The schema sets `additionalProperties: false`, so a field documented in + SKILL.md but absent here is a field a user is told to state and that then + fails validation. + """ + vocabulary = re.search( + r"The vocabulary is closed.*?```yaml\n(.*?)\n```", SKILL_PATH.read_text(), re.DOTALL + ) + assert vocabulary, "SKILL.md §2 no longer publishes the closed vocabulary block" + + documented = set(re.findall(r"^(\w+):", vocabulary.group(1), re.MULTILINE)) + missing = documented - set(schema["properties"]) + assert not missing, f"documented in SKILL.md but rejected by the schema: {sorted(missing)}" + + +@pytest.mark.parametrize("name,profile", example_profiles(), ids=lambda v: v if isinstance(v, str) else "") +def test_published_example_profiles_validate(schema, name, profile): + """An example profile that fails its own schema teaches the wrong shape.""" + jsonschema.validate(profile, schema) + + +def test_example_profiles_were_actually_found(): + assert len(example_profiles()) >= 3 + + +def test_array_fields_accept_unknown(schema): + """SKILL.md: 'Every field is tri-state'. That has to include list fields — + 'we deploy somewhere, I do not know where' is a real answer.""" + jsonschema.validate({"deploy": {"environments": "unknown"}}, schema) + jsonschema.validate({"languages": "unknown"}, schema) + + +def test_fields_rules_depend_on_are_accepted(schema): + """Each of these carries a rule's applicability and was missing before.""" + jsonschema.validate({"source": {"fork_prs_allowed": True}}, schema) # DSB-SRC-003 + jsonschema.validate({"policy": {"signing_required": True}}, schema) # DSB-ART-002 + jsonschema.validate({"policy": {"provenance_required": True}}, schema) # DSB-ART-003 + jsonschema.validate({"registry": {"controlled_proxy": True}}, schema) # DSB-SC-001 + jsonschema.validate({"cicd": {"shared_library": True}}, schema) + + def test_existing_controls_accepts_multiple_tools_per_capability(schema): profile = yaml.safe_load( """ diff --git a/tests/test_readme.py b/tests/test_readme.py new file mode 100644 index 0000000..9a18ea2 --- /dev/null +++ b/tests/test_readme.py @@ -0,0 +1,61 @@ +import pathlib +import re + +import pytest + +ROOT = pathlib.Path(__file__).resolve().parents[1] +README = ROOT / "README.md" +DOCS = ROOT / "docs" + +TEXT = README.read_text() + + +def slug(heading): + """GitHub's heading-anchor rule: lowercase, punctuation dropped, spaces hyphenated.""" + s = heading.strip().lower() + s = re.sub(r"[^\w\s-]", "", s) + return re.sub(r"\s+", "-", s) + + +def headings(): + return {slug(h) for h in re.findall(r"^#{2,3} (.+)$", TEXT, re.MULTILINE)} + + +def toc_anchors(): + block = re.search(r"^## Contents\n(.*?)^---", TEXT, re.MULTILINE | re.DOTALL) + assert block, "README.md has no '## Contents' table of contents" + return re.findall(r"\]\(#([\w-]+)\)", block.group(1)) + + +def doc_files(): + return sorted(p for p in DOCS.rglob("*.md")) + + +def test_table_of_contents_exists_and_is_not_empty(): + assert len(toc_anchors()) >= 5 + + +@pytest.mark.parametrize("anchor", toc_anchors()) +def test_every_toc_entry_points_at_a_real_heading(anchor): + """A table of contents whose links go nowhere is worse than none — it looks + navigable and silently drops the reader at the top of the page.""" + assert anchor in headings(), f"'#{anchor}' matches no heading in README.md" + + +def test_every_section_is_listed_in_the_contents(): + """Adding a section without listing it makes the contents quietly incomplete.""" + listed = set(toc_anchors()) + missing = headings() - listed - {"contents"} + assert not missing, f"sections missing from the table of contents: {sorted(missing)}" + + +@pytest.mark.parametrize("path", doc_files(), ids=lambda p: str(p.relative_to(ROOT))) +def test_every_docs_file_is_linked_from_the_readme(path): + """An unlinked document is an unread one. The README is the only entry point + most people use, so docs/ has to be reachable from it.""" + rel = str(path.relative_to(ROOT)) + assert f"({rel})" in TEXT, f"{rel} is not linked from README.md" + + +def test_docs_files_were_actually_found(): + assert len(doc_files()) >= 6 diff --git a/tools/generate_mappings.py b/tools/generate_mappings.py index 2f9da3a..fd0a161 100644 --- a/tools/generate_mappings.py +++ b/tools/generate_mappings.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Generate references/framework-mappings.md from SKILL.md. +"""Generate docs/framework-mappings.md from SKILL.md. The per-rule mappings in SKILL.md are the source of truth. This script inverts them into a framework-first index, so an auditor asking "which DSB rules cover @@ -24,7 +24,7 @@ from validate_skill import ROOT, SKILL, parse_rules # noqa: E402 -OUTPUT = ROOT / "references" / "framework-mappings.md" +OUTPUT = ROOT / "docs" / "framework-mappings.md" # Order matters: "OWASP SAMM" must be tested before the bare "OWASP " prefix, # which is how OWASP CI/CD controls are written. @@ -140,7 +140,7 @@ def render(rules): def main(): parser = argparse.ArgumentParser( - description="Generate references/framework-mappings.md from SKILL.md.", + description="Generate docs/framework-mappings.md from SKILL.md.", epilog="Example: python tools/generate_mappings.py --check", ) parser.add_argument("--check", action="store_true", diff --git a/tools/requirements.txt b/tools/requirements.txt index 35dccdc..e5a2a39 100644 --- a/tools/requirements.txt +++ b/tools/requirements.txt @@ -1,3 +1,13 @@ -PyYAML>=6.0.2 -jsonschema>=4.23.0 -pytest>=8.3.0 +# Pinned exactly, not floored with `>=`. +# +# DSB-BUILD-002 (BLOCK) requires dependency resolution to be repeatable: the +# same commit must produce the same dependency set. A `>=` floor means CI +# resolves whatever was published that morning, so a run that passes today and +# fails tomorrow tells you nothing about the commit. This repository applies its +# own rules. +# +# Dependabot (see .github/dependabot.yml) proposes the bumps, which is what +# keeps pinning maintainable rather than a one-time gesture. +PyYAML==6.0.3 +jsonschema==4.26.0 +pytest==9.1.1