bmad-plus 0.20.0 → 0.22.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +41 -0
- package/README.md +14 -14
- package/SECURITY.md +62 -0
- package/osint-agent-package/skills/bmad-osint-investigate/osint/scripts/_http.py +68 -24
- package/package.json +1 -1
- package/readme-international/README.de.md +14 -14
- package/readme-international/README.es.md +14 -14
- package/readme-international/README.fr.md +14 -14
- package/src/bmad-plus/agents/agent-quality/SKILL.md +1 -1
- package/src/bmad-plus/packs/pack-dev-studio/categories/implementation/code-review.md +26 -5
- package/src/bmad-plus/packs/pack-seo/SKILL.md +3 -1
- package/src/bmad-plus/packs/pack-seo/ref/cwv-thresholds.md +2 -2
- package/src/bmad-plus/packs/pack-seo/requirements.txt +1 -1
- package/src/bmad-plus/packs/pack-seo/scripts/seo_apis.py +72 -30
- package/src/bmad-plus/packs/pack-seo/scripts/seo_crawl.py +36 -24
- package/src/bmad-plus/packs/pack-seo/scripts/seo_fetch.py +179 -59
- package/src/bmad-plus/packs/pack-seo/scripts/seo_report.py +5 -6
- package/src/bmad-plus/packs/pack-seo/scripts/seo_screenshot.py +176 -14
- package/src/bmad-plus/packs/pack-shield/README.md +12 -0
- package/src/bmad-plus/packs/pack-shield/SKILL.md +7 -1
- package/src/bmad-plus/packs/pack-shield/review-rules/access-control.md +10 -0
- package/src/bmad-plus/packs/pack-shield/review-rules/ai-integrations.md +10 -0
- package/src/bmad-plus/packs/pack-shield/review-rules/change-and-supply-chain.md +10 -0
- package/src/bmad-plus/packs/pack-shield/review-rules/cryptography.md +10 -0
- package/src/bmad-plus/packs/pack-shield/review-rules/index.yaml +134 -0
- package/src/bmad-plus/packs/pack-shield/review-rules/logging.md +10 -0
- package/src/bmad-plus/packs/pack-shield/review-rules/personal-data.md +10 -0
- package/src/bmad-plus/packs/pack-shield/shared/ai-processing-register-template.yaml +53 -0
- package/src/bmad-plus/packs/pack-shield/shared/ai-processing-register.md +32 -0
- package/src/bmad-plus/packs/pack-shield/shared/assurance-case-template.yaml +87 -0
- package/src/bmad-plus/packs/pack-shield/shared/assurance-case.md +50 -0
- package/src/bmad-plus/packs/pack-shield/shield-orchestrator.md +24 -1
- package/src/bmad-plus/skills/bmad-plus-uat/SKILL.md +1 -0
- package/src/bmad-plus/skills/bmad-plus-uat/template/page.html +5 -4
- package/tools/build/generate-adapters.js +7 -0
- package/tools/build/generate.js +14 -0
- package/tools/cli/bmad-plus-cli.js +2 -0
- package/tools/cli/commands/ai-register.js +63 -0
- package/tools/cli/commands/assurance.js +162 -0
- package/tools/cli/commands/review.js +141 -7
- package/tools/cli/lib/ai-register.js +393 -0
- package/tools/cli/lib/assurance.js +822 -0
- package/tools/cli/lib/control-refs.js +132 -0
- package/tools/cli/lib/installation-health.js +17 -0
- package/tools/cli/lib/packs.js +60 -2
- package/tools/cli/lib/page-origins.js +582 -0
- package/tools/cli/lib/review-rules.js +124 -26
- package/tools/cli/lib/review.js +493 -10
- package/tools/cli/lib/uat.js +22 -5
- package/tools/cli/review-rules/index.yaml +9 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Review checklists chosen by path. Built-in rules ship with the CLI;
|
|
3
|
-
*
|
|
4
|
-
* a file applies to it — rules
|
|
5
|
-
*
|
|
2
|
+
* Review checklists chosen by path. Built-in rules ship with the CLI; an installed pack may
|
|
3
|
+
* add its own (Shield's compliance rules); a project adds, replaces or disables rules in
|
|
4
|
+
* `_bmad/review-rules.yaml`. Every rule whose patterns match a file applies to it — rules
|
|
5
|
+
* add up, so a broad project rule never silences the built-in ones by accident. A rule may
|
|
6
|
+
* name the compliance controls it examines. The resolved set is hashed into the review scope.
|
|
6
7
|
*/
|
|
7
8
|
'use strict';
|
|
8
9
|
|
|
@@ -11,12 +12,16 @@ const path = require('node:path');
|
|
|
11
12
|
const crypto = require('node:crypto');
|
|
12
13
|
const yaml = require('js-yaml');
|
|
13
14
|
const { compile, matchesAny } = require('./glob');
|
|
15
|
+
const { parseControls } = require('./control-refs');
|
|
16
|
+
const { PACKS } = require('./packs');
|
|
14
17
|
|
|
15
18
|
const SCHEMA = 'bmad-plus/review-rules/1';
|
|
16
19
|
const BUILTIN_DIR = path.join(__dirname, '..', 'review-rules');
|
|
17
20
|
const PROJECT_FILE = path.join('_bmad', 'review-rules.yaml');
|
|
21
|
+
/** Where an installed pack keeps its rules, below its own folder in `.agents/skills`. */
|
|
22
|
+
const PACK_RULES_DIR = 'review-rules';
|
|
18
23
|
const RULE_ID = /^[a-z0-9][a-z0-9-]{0,60}$/;
|
|
19
|
-
const RULE_KEYS = ['id', 'title', 'globs', 'doc'];
|
|
24
|
+
const RULE_KEYS = ['id', 'title', 'group', 'globs', 'doc', 'controls'];
|
|
20
25
|
const MAX_DOC_BYTES = 64 * 1024;
|
|
21
26
|
const MAX_INDEX_BYTES = 256 * 1024;
|
|
22
27
|
|
|
@@ -47,8 +52,8 @@ function parseLayer(root, indexName, layer) {
|
|
|
47
52
|
if (!doc || doc.schema !== SCHEMA) throw new Error(`${where}: schema must be "${SCHEMA}"`);
|
|
48
53
|
const extra = Object.keys(doc).filter((key) => !['schema', 'rules', 'disable'].includes(key));
|
|
49
54
|
if (extra.length) throw new Error(`${where}: unknown key(s) ${extra.join(', ')}`);
|
|
50
|
-
if (layer
|
|
51
|
-
throw new Error(`${where}:
|
|
55
|
+
if (layer !== 'project' && doc.disable)
|
|
56
|
+
throw new Error(`${where}: only a project's rules can disable`);
|
|
52
57
|
const disable = doc.disable || [];
|
|
53
58
|
if (!Array.isArray(disable) || !disable.every((id) => typeof id === 'string'))
|
|
54
59
|
throw new Error(`${where}: disable must be a list of rule ids`);
|
|
@@ -60,11 +65,13 @@ function parseLayer(root, indexName, layer) {
|
|
|
60
65
|
if (!rule || typeof rule !== 'object') throw new Error(`${at}: must be a mapping`);
|
|
61
66
|
const unknown = Object.keys(rule).filter((key) => !RULE_KEYS.includes(key));
|
|
62
67
|
if (unknown.length) throw new Error(`${at}: unknown key(s) ${unknown.join(', ')}`);
|
|
63
|
-
if (!RULE_ID.test(
|
|
68
|
+
if (typeof rule.id !== 'string' || !RULE_ID.test(rule.id)) throw new Error(`${at}: invalid id`);
|
|
64
69
|
if (seen.has(rule.id)) throw new Error(`${at}: duplicate id`);
|
|
65
70
|
seen.add(rule.id);
|
|
66
71
|
if (typeof rule.title !== 'string' || !rule.title.trim())
|
|
67
72
|
throw new Error(`${at}: title is required`);
|
|
73
|
+
if (rule.group !== undefined && !(typeof rule.group === 'string' && RULE_ID.test(rule.group)))
|
|
74
|
+
throw new Error(`${at}: invalid group`);
|
|
68
75
|
if (
|
|
69
76
|
!Array.isArray(rule.globs) ||
|
|
70
77
|
!rule.globs.length ||
|
|
@@ -72,13 +79,17 @@ function parseLayer(root, indexName, layer) {
|
|
|
72
79
|
)
|
|
73
80
|
throw new Error(`${at}: globs must be a non-empty list of patterns`);
|
|
74
81
|
for (const glob of rule.globs) compile(glob);
|
|
82
|
+
const controls = rule.controls === undefined ? [] : parseControls(rule.controls, at);
|
|
75
83
|
if (!/\.md$/i.test(String(rule.doc))) throw new Error(`${at}: doc must be a Markdown file`);
|
|
76
84
|
const text = readConfined(root, rule.doc, MAX_DOC_BYTES, at).trim();
|
|
77
85
|
if (!text) throw new Error(`${at}: ${rule.doc} is empty`);
|
|
78
86
|
return {
|
|
79
87
|
id: rule.id,
|
|
80
88
|
title: rule.title.trim(),
|
|
89
|
+
// A rule family reviewers can split by; a rule without one is a family of its own.
|
|
90
|
+
group: rule.group === undefined ? rule.id : rule.group,
|
|
81
91
|
globs: [...rule.globs],
|
|
92
|
+
controls,
|
|
82
93
|
layer,
|
|
83
94
|
source: rule.doc,
|
|
84
95
|
text,
|
|
@@ -87,13 +98,35 @@ function parseLayer(root, indexName, layer) {
|
|
|
87
98
|
return { rules: parsed, disable };
|
|
88
99
|
}
|
|
89
100
|
|
|
101
|
+
/** Rule indexes of the packs installed in the project, in pack order. */
|
|
102
|
+
function installedPackLayers(projectDir) {
|
|
103
|
+
const layers = [];
|
|
104
|
+
for (const [pack, { packDir }] of Object.entries(PACKS)) {
|
|
105
|
+
if (!packDir) continue;
|
|
106
|
+
const relative = ['.agents', 'skills', packDir, PACK_RULES_DIR, 'index.yaml'];
|
|
107
|
+
if (fs.existsSync(path.join(projectDir, ...relative)))
|
|
108
|
+
layers.push({ pack, file: relative.join('/') });
|
|
109
|
+
}
|
|
110
|
+
return layers;
|
|
111
|
+
}
|
|
112
|
+
|
|
90
113
|
/**
|
|
91
|
-
* The effective rule set for a project: built-in rules, then
|
|
92
|
-
*
|
|
114
|
+
* The effective rule set for a project: built-in rules, then those of installed packs, then
|
|
115
|
+
* the project's own. A pack only adds; a project rule with an existing id replaces it, and
|
|
116
|
+
* `disable` removes rules by id and is reported.
|
|
93
117
|
*/
|
|
94
118
|
function loadRuleset(projectDir, { builtinDir = BUILTIN_DIR } = {}) {
|
|
95
119
|
const builtin = parseLayer(builtinDir, 'index.yaml', 'builtin');
|
|
96
120
|
const ordered = new Map(builtin.rules.map((rule) => [rule.id, rule]));
|
|
121
|
+
const packFiles = installedPackLayers(projectDir);
|
|
122
|
+
for (const { pack, file } of packFiles) {
|
|
123
|
+
const layer = parseLayer(path.join(projectDir, path.dirname(file)), 'index.yaml', 'pack');
|
|
124
|
+
for (const rule of layer.rules) {
|
|
125
|
+
if (ordered.has(rule.id))
|
|
126
|
+
throw new Error(`${pack} review rules: rule ${rule.id} already exists; a pack only adds`);
|
|
127
|
+
ordered.set(rule.id, { ...rule, pack });
|
|
128
|
+
}
|
|
129
|
+
}
|
|
97
130
|
let disabled = [];
|
|
98
131
|
const projectIndex = path.join(projectDir, PROJECT_FILE);
|
|
99
132
|
let project = null;
|
|
@@ -107,10 +140,20 @@ function loadRuleset(projectDir, { builtinDir = BUILTIN_DIR } = {}) {
|
|
|
107
140
|
for (const id of disabled) ordered.delete(id);
|
|
108
141
|
}
|
|
109
142
|
const list = [...ordered.values()];
|
|
110
|
-
|
|
143
|
+
// Controls join the fingerprint only when a rule names some, so a rule set without any
|
|
144
|
+
// keeps the hash it had before rules could carry controls.
|
|
145
|
+
const fingerprint = list.map((rule) => [
|
|
146
|
+
rule.id,
|
|
147
|
+
rule.group,
|
|
148
|
+
rule.layer,
|
|
149
|
+
rule.globs,
|
|
150
|
+
sha256(rule.text),
|
|
151
|
+
...(rule.controls.length ? [rule.controls] : []),
|
|
152
|
+
]);
|
|
111
153
|
return {
|
|
112
154
|
rules: list,
|
|
113
155
|
disabled,
|
|
156
|
+
packFiles: packFiles.map(({ file }) => file),
|
|
114
157
|
projectFile: project ? PROJECT_FILE.split(path.sep).join('/') : null,
|
|
115
158
|
sha256: sha256(JSON.stringify({ rules: fingerprint, disabled })),
|
|
116
159
|
};
|
|
@@ -121,36 +164,91 @@ function rulesFor(ruleset, file) {
|
|
|
121
164
|
return ruleset.rules.filter((rule) => matchesAny(file, rule.globs)).map((rule) => rule.id);
|
|
122
165
|
}
|
|
123
166
|
|
|
167
|
+
/** The rule families of a set of rule ids, each with its rules, in rule order. */
|
|
168
|
+
function groupsOf(ruleset, ids) {
|
|
169
|
+
const groups = new Map();
|
|
170
|
+
for (const rule of ruleset.rules) {
|
|
171
|
+
if (!ids.includes(rule.id)) continue;
|
|
172
|
+
groups.set(rule.group, [...(groups.get(rule.group) || []), rule.id]);
|
|
173
|
+
}
|
|
174
|
+
return [...groups].map(([id, rules]) => ({ id, rules }));
|
|
175
|
+
}
|
|
176
|
+
|
|
124
177
|
/**
|
|
125
|
-
* The
|
|
126
|
-
*
|
|
178
|
+
* The compliance controls a set of rule ids examines, each with the rules that name it, in
|
|
179
|
+
* rule order. A control touched is a prompt to look at its requirement, not a finding.
|
|
180
|
+
*/
|
|
181
|
+
function controlsOf(ruleset, ids) {
|
|
182
|
+
const controls = new Map();
|
|
183
|
+
for (const rule of ruleset.rules) {
|
|
184
|
+
if (!ids.includes(rule.id)) continue;
|
|
185
|
+
for (const control of rule.controls)
|
|
186
|
+
controls.set(control, [...(controls.get(control) || []), rule.id]);
|
|
187
|
+
}
|
|
188
|
+
return [...controls].map(([id, rules]) => ({ id, rules }));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* The reviewer's checklist for a scope: the controls the change touches, then each
|
|
193
|
+
* applicable rule once, with the files it covers. Rules that match nothing are left out.
|
|
127
194
|
*/
|
|
128
195
|
function checklist(ruleset, byPath, { title = 'Review checklist' } = {}) {
|
|
129
196
|
const files = new Map();
|
|
130
197
|
for (const [file, ids] of Object.entries(byPath)) {
|
|
131
198
|
for (const id of ids) files.set(id, [...(files.get(id) || []), file]);
|
|
132
199
|
}
|
|
133
|
-
const
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
200
|
+
const applied = ruleset.rules.filter((rule) => files.has(rule.id));
|
|
201
|
+
const controls = controlsOf(
|
|
202
|
+
ruleset,
|
|
203
|
+
applied.map((rule) => rule.id)
|
|
204
|
+
);
|
|
205
|
+
const touched = controls.length
|
|
206
|
+
? [
|
|
207
|
+
'## Controls this change touches',
|
|
208
|
+
'',
|
|
209
|
+
...controls.map(
|
|
210
|
+
({ id, rules }) => `- \`${id}\` — ${rules.map((rule) => `\`${rule}\``).join(', ')}`
|
|
211
|
+
),
|
|
138
212
|
'',
|
|
139
|
-
`
|
|
140
|
-
.get(rule.id)
|
|
141
|
-
.map((file) => `\`${file}\``)
|
|
142
|
-
.join(', ')}`,
|
|
213
|
+
'List in the `controls` of a finding the control it breaks; a control listed here is not a finding.',
|
|
143
214
|
'',
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
215
|
+
]
|
|
216
|
+
: [];
|
|
217
|
+
const sections = applied.map((rule) =>
|
|
218
|
+
[
|
|
219
|
+
`## ${rule.title} \`${rule.id}\`${rule.layer === 'project' ? ' (project rule)' : ''}${rule.pack ? ` (${rule.pack} rule)` : ''}`,
|
|
220
|
+
'',
|
|
221
|
+
`Group: \`${rule.group}\``,
|
|
222
|
+
...(rule.controls.length
|
|
223
|
+
? ['', `Controls: ${rule.controls.map((control) => `\`${control}\``).join(', ')}`]
|
|
224
|
+
: []),
|
|
225
|
+
'',
|
|
226
|
+
`Applies to: ${files
|
|
227
|
+
.get(rule.id)
|
|
228
|
+
.map((file) => `\`${file}\``)
|
|
229
|
+
.join(', ')}`,
|
|
230
|
+
'',
|
|
231
|
+
rule.text,
|
|
232
|
+
].join('\n')
|
|
233
|
+
);
|
|
147
234
|
return [
|
|
148
235
|
`# ${title}`,
|
|
149
236
|
'',
|
|
150
237
|
`Rule set ${ruleset.sha256.slice(0, 12)}. Report a finding only with its quoted code; a rule is a prompt to look, not a finding.`,
|
|
151
238
|
'',
|
|
239
|
+
...touched,
|
|
152
240
|
...sections.flatMap((section) => [section, '']),
|
|
153
241
|
].join('\n');
|
|
154
242
|
}
|
|
155
243
|
|
|
156
|
-
module.exports = {
|
|
244
|
+
module.exports = {
|
|
245
|
+
SCHEMA,
|
|
246
|
+
BUILTIN_DIR,
|
|
247
|
+
PROJECT_FILE,
|
|
248
|
+
PACK_RULES_DIR,
|
|
249
|
+
loadRuleset,
|
|
250
|
+
rulesFor,
|
|
251
|
+
groupsOf,
|
|
252
|
+
controlsOf,
|
|
253
|
+
checklist,
|
|
254
|
+
};
|