bmad-plus 0.9.1 → 0.9.2

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 CHANGED
@@ -5,6 +5,27 @@ All notable changes to BMAD+ will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [0.9.2] — 2026-07-01
9
+
10
+ ### Security — Phase 1 remediation (exploitable findings)
11
+ - **RCE closed** (`ci_cd.py`): command allowlist now uses exact-token matching (shlex.split) + realpath/commonpath containment instead of prefix/`startswith` — defeats argument injection (`make -f attacker.mk`) and sibling-path bypass; `shell=False` throughout; dead Makefile fallback removed.
12
+ - **SSRF hardened** (`seo_fetch.py`): fails **closed** on DNS error, blocks private/loopback/reserved/link-local IPs (incl. IPv4-mapped IPv6), re-validates every redirect hop manually.
13
+ - **Stored + DOM XSS closed** (`seo_report.py`, `dashboard.html`): all audited/external values HTML-escaped in the report; dashboard renders via `textContent`/`createElement` + `addEventListener` instead of `innerHTML`/inline `onclick`.
14
+ - **MCP server** (`server.py`): constant-time `hmac.compare_digest` for token + dashboard password; app assembled in module-level `create_app()` so `uvicorn server:app` runs with auth intact; dead `verify_mcp_token` removed; rate-limit map eviction.
15
+ - **XXE**: `seo_crawl.py` hard-fails without `defusedxml` (no unsafe stdlib fallback).
16
+
17
+ ### Supply chain
18
+ - `requests>=2.32.4` (CVE-2024-35195) across all requirements files.
19
+ - `PyPDF2`→`pypdf==4.3.1` (dep + consumer import in `gamma_report.py`).
20
+ - `defusedxml==0.7.1` added; Dockerfile non-root user + tag pin; docker-compose off `:latest`; GitHub Actions pinned to commit SHAs; Dependabot covers pip + docker + github-actions.
21
+
22
+ ### Fixed — robustness
23
+ - Non-destructive install (backs up existing CLAUDE/GEMINI/AGENTS.md); marker-based uninstall; IDE-scoped autoconfig; `doctor` no longer emits false "Missing agent" warnings; guarded manifest `JSON.parse`; Windows-safe project-path hashing; hardened `validateUserName`; lazy RAG model load; async gamma polling; repo-URL validation; `git_ops` path confinement.
24
+
25
+ ### Notes
26
+ - Documented residual follow-ups (SSRF crawler-class redirect hops, DNS-rebinding IP pinning, Dockerfile digest pin) are tracked in `audit/2026-07-01/PHASE-1-STATUS.md`.
27
+ - Verified: `npm test` 176/176, `npm run lint` 0 errors, all edited Python parses; SEO report smoke-tested (renders + XSS escaped).
28
+
8
29
  ## [0.9.1] — 2026-07-01
9
30
 
10
31
  ### Fixed — Credibility (honest status)
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # 🚀 BMAD+ — Augmented Multi-Agent AI Framework
2
2
 
3
- [![Version](https://img.shields.io/badge/version-0.9.0-blue.svg)](CHANGELOG.md)
3
+ [![Version](https://img.shields.io/badge/version-0.9.2-blue.svg)](CHANGELOG.md)
4
4
  [![Based on](https://img.shields.io/badge/based%20on-BMAD--METHOD-green.svg)](https://github.com/bmad-code-org/BMAD-METHOD)
5
5
  [![License](https://img.shields.io/badge/license-MIT-yellow.svg)](LICENSE)
6
6
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "bmad-plus",
4
- "version": "0.9.1",
4
+ "version": "0.9.2",
5
5
  "description": "BMAD+ — Augmented AI-Driven Development Framework with multi-role agents, autopilot, and parallel execution",
6
6
  "keywords": [
7
7
  "bmad",
@@ -11,6 +11,7 @@ const fs = require('node:fs');
11
11
  const clack = require('@clack/prompts');
12
12
  const pc = require('picocolors');
13
13
  const { detectStack } = require('../lib/stack-detect');
14
+ const { IDE_CONFIGS } = require('../lib/ide-config');
14
15
 
15
16
  // ── Project Analysis Engine ──
16
17
 
@@ -282,7 +283,23 @@ module.exports = {
282
283
 
283
284
  // Use the install module directly
284
285
  const installModule = require('./install');
285
- const toolsArg = structure.hasIdeConfigs.length > 0 ? 'none' : undefined;
286
+
287
+ // NODE-06: only generate IDE configs for IDEs this project actually uses.
288
+ // - If IDE config files already exist, preserve them ('none').
289
+ // - Otherwise detect IDE markers and pass only those; never let install's
290
+ // --yes fallback write ALL three configs into a project that uses none.
291
+ let toolsArg;
292
+ if (structure.hasIdeConfigs.length > 0) {
293
+ toolsArg = 'none';
294
+ } else {
295
+ const detectedIDEs = [];
296
+ for (const [id, ide] of Object.entries(IDE_CONFIGS)) {
297
+ if (ide.detect.some(marker => fs.existsSync(path.join(projectDir, marker)))) {
298
+ detectedIDEs.push(id);
299
+ }
300
+ }
301
+ toolsArg = detectedIDEs.length > 0 ? detectedIDEs.join(',') : 'none';
302
+ }
286
303
 
287
304
  // Build install args
288
305
  try {
@@ -88,6 +88,18 @@ module.exports = {
88
88
  const packPath = path.join(agentsDir, entry.packDir);
89
89
  if (fs.existsSync(packPath)) {
90
90
  passed++;
91
+
92
+ // These packs bundle their agents as files inside packDir — verify
93
+ // them there (not as loose directories, which would falsely warn).
94
+ for (const agentFile of (entry.packAgents || [])) {
95
+ checks++;
96
+ if (fs.existsSync(path.join(packPath, agentFile))) {
97
+ passed++;
98
+ } else {
99
+ clack.log.warn(`⚠️ Missing agent: ${agentFile} (pack: ${pack})`);
100
+ warnings++;
101
+ }
102
+ }
91
103
  } else {
92
104
  clack.log.warn(`⚠️ Missing pack directory: ${entry.packDir} (pack: ${pack})`);
93
105
  warnings++;
@@ -246,12 +246,32 @@ module.exports = {
246
246
  ideSpinner.start(i.configuring_ides);
247
247
 
248
248
  const configContent = generateIDEConfig(userName, commLang, selectedPacks);
249
+ // Marker present in every BMAD+-generated IDE config (see lib/ide-config.js).
250
+ const BMAD_MARKER = 'BMAD+ — AI Agent Configuration';
249
251
 
250
252
  for (const ideId of detectedIDEs) {
251
253
  const ide = IDE_CONFIGS[ideId];
252
254
  if (!ide) continue;
253
255
 
254
256
  const configPath = path.join(projectDir, ide.configFile);
257
+
258
+ // NODE-02: never clobber a hand-authored config without protecting it.
259
+ if (fs.existsSync(configPath)) {
260
+ const existing = fs.readFileSync(configPath, 'utf8');
261
+ if (!existing.includes(BMAD_MARKER)) {
262
+ if (!options.yes) {
263
+ // Interactive: keep the user's file untouched.
264
+ clack.log.warn(`⚠️ ${ide.configFile} already exists and was not created by BMAD+ — kept your file (skipped). Re-run with --yes to back it up and overwrite.`);
265
+ continue;
266
+ }
267
+ // Non-interactive: back up before overwriting, never destroy data.
268
+ let backupPath = `${configPath}.bak`;
269
+ if (fs.existsSync(backupPath)) backupPath = `${configPath}.${Date.now()}.bak`;
270
+ fs.copyFileSync(configPath, backupPath);
271
+ clack.log.warn(`⚠️ Backed up existing ${ide.configFile} → ${path.basename(backupPath)}`);
272
+ }
273
+ }
274
+
255
275
  fs.writeFileSync(configPath, configContent, 'utf8');
256
276
  }
257
277
 
@@ -118,6 +118,22 @@ function scanDirectory(rootDir, maxDepth = 4, currentDepth = 0, activeDays = 30,
118
118
  return projects;
119
119
  }
120
120
 
121
+ /**
122
+ * Normalize a filesystem path for stable hashing.
123
+ * On Windows the same project can be referenced as `D:\proj` or `d:\proj`
124
+ * (case-insensitive drive) with mixed separators. Resolving + lowercasing the
125
+ * drive letter ensures the same project produces the same index key and is not
126
+ * double-indexed.
127
+ * @param {string} p - Raw filesystem path
128
+ * @returns {string} Normalized path suitable for hashing
129
+ */
130
+ function normalizePathForHash(p) {
131
+ let resolved = path.resolve(p);
132
+ // Lowercase a leading Windows drive letter (e.g. "D:" → "d:").
133
+ resolved = resolved.replace(/^([A-Za-z]):/, (_m, drive) => drive.toLowerCase() + ':');
134
+ return resolved;
135
+ }
136
+
121
137
  /**
122
138
  * Index a single project in the global brain by writing its metadata YAML file.
123
139
  * @param {object} project - Project metadata object
@@ -125,7 +141,7 @@ function scanDirectory(rootDir, maxDepth = 4, currentDepth = 0, activeDays = 30,
125
141
  * @returns {void}
126
142
  */
127
143
  function indexProject(project, globalBrainDir) {
128
- const hash = crypto.createHash('sha256').update(project.path).digest('hex').slice(0, 8);
144
+ const hash = crypto.createHash('sha256').update(normalizePathForHash(project.path)).digest('hex').slice(0, 8);
129
145
  const meta = {
130
146
  path: project.path,
131
147
  name: project.name,
@@ -341,6 +357,7 @@ module.exports = {
341
357
  getProjectName,
342
358
  hasBmadInstalled,
343
359
  scanDirectory,
360
+ normalizePathForHash,
344
361
  indexProject,
345
362
  indexProjects,
346
363
  },
@@ -86,7 +86,9 @@ module.exports = {
86
86
  const p = path.join(projectDir, configFile);
87
87
  if (fs.existsSync(p)) {
88
88
  const content = fs.readFileSync(p, 'utf8');
89
- if (content.includes('BMAD+')) {
89
+ // NODE-05: only remove a config BMAD+ actually generated (it carries the
90
+ // marker below). A hand-authored file that merely mentions "BMAD+" is kept.
91
+ if (content.includes('BMAD+ — AI Agent Configuration')) {
90
92
  fs.unlinkSync(p);
91
93
  removed++;
92
94
  }
@@ -39,7 +39,21 @@ module.exports = {
39
39
  return;
40
40
  }
41
41
 
42
- const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
42
+ let manifest;
43
+ try {
44
+ manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
45
+ } catch (err) {
46
+ clack.log.error(`Install manifest is unreadable or corrupt: ${err.message}`);
47
+ clack.log.info('Re-run `npx bmad-plus install` to repair the installation.');
48
+ clack.outro(pc.red('Update aborted.'));
49
+ return;
50
+ }
51
+ if (!manifest || typeof manifest !== 'object') {
52
+ clack.log.error('Install manifest is malformed (not an object).');
53
+ clack.log.info('Re-run `npx bmad-plus install` to repair the installation.');
54
+ clack.outro(pc.red('Update aborted.'));
55
+ return;
56
+ }
43
57
  const lang = options.lang || manifest.uiLanguage || 'en';
44
58
  const i = t(lang);
45
59
 
@@ -51,7 +65,10 @@ module.exports = {
51
65
  return;
52
66
  }
53
67
 
54
- const selectedPacks = manifest.packs || ['core'];
68
+ // Guard against a missing/malformed `packs` field (NODE-04): coerce to an array.
69
+ const selectedPacks = Array.isArray(manifest.packs) && manifest.packs.length > 0
70
+ ? manifest.packs
71
+ : ['core'];
55
72
  clack.log.info(`${i.selected_packs}: ${selectedPacks.join(', ')}`);
56
73
 
57
74
  const confirm = await clack.confirm({
@@ -1,114 +1,122 @@
1
- /**
2
- * BMAD+ Shared PACKS Module
3
- * Single source of truth for pack definitions, expected agents, and pack ordering.
4
- *
5
- * Author: Laurent Rochetta
6
- */
7
-
8
- const PACKS = {
9
- core: {
10
- name: 'Core',
11
- icon: 'b',
12
- agents: ['agent-strategist', 'agent-architect-dev', 'agent-quality', 'agent-orchestrator'],
13
- skills: ['bmad-plus-autopilot', 'bmad-plus-parallel', 'bmad-plus-sync'],
14
- data: ['role-triggers.yaml'],
15
- packDir: 'pack-core',
16
- packSrcDir: 'packs',
17
- required: true,
18
- desc: 'Core agents & skills',
19
- },
20
- osint: {
21
- name: 'OSINT',
22
- icon: 'j',
23
- agents: ['agent-shadow'],
24
- skills: [],
25
- externalPackage: 'osint-agent-package',
26
- packDir: 'pack-osint',
27
- packSrcDir: 'packs',
28
- desc: 'OSINT & investigation',
29
- },
30
- maker: {
31
- name: 'Maker',
32
- icon: 'f',
33
- agents: ['agent-maker'],
34
- skills: [],
35
- data: [],
36
- packDir: 'pack-maker',
37
- packSrcDir: 'packs',
38
- desc: 'Agent creation toolkit',
39
- },
40
- shield: {
41
- name: 'Shield',
42
- icon: 'm',
43
- agents: ['shield-orchestrator'],
44
- skills: [],
45
- packDir: 'pack-shield',
46
- packSrcDir: 'packs',
47
- desc: 'GRC compliance (25+ frameworks)',
48
- },
49
- seo: {
50
- name: 'SEO',
51
- icon: 'k',
52
- agents: ['seo-scout', 'seo-chief', 'seo-judge'],
53
- skills: [],
54
- packDir: 'pack-seo',
55
- packSrcDir: 'packs',
56
- desc: 'SEO audit & optimization',
57
- },
58
- memory: {
59
- name: 'Memory',
60
- icon: 'x',
61
- agents: ['zecher'],
62
- skills: [],
63
- packDir: 'pack-memory',
64
- packSrcDir: 'packs',
65
- desc: 'Persistent cross-session memory',
66
- },
67
- 'dev-studio': {
68
- name: 'Dev Studio',
69
- icon: 'v',
70
- agents: ['dev-studio-orchestrator'],
71
- skills: ['dev-studio'],
72
- packDir: 'pack-dev-studio',
73
- packSrcDir: 'packs',
74
- desc: 'SDLC automation (6 agents, 56+ skills)',
75
- },
76
- backup: {
77
- name: 'Backup',
78
- icon: 'y',
79
- agents: ['backup-agent'],
80
- skills: [],
81
- packDir: 'pack-backup',
82
- packSrcDir: 'packs',
83
- desc: 'Backup & restore',
84
- },
85
- animated: {
86
- name: 'Animated',
87
- icon: 'z',
88
- agents: ['animated-website-agent'],
89
- skills: [],
90
- packDir: 'pack-animated',
91
- packSrcDir: 'packs',
92
- desc: 'Animated website agents',
93
- },
94
- };
95
-
96
- const PACK_ORDER = ['core', 'osint', 'maker', 'shield', 'seo', 'memory', 'dev-studio', 'backup', 'animated'];
97
-
98
- /**
99
- * Maps each pack to the list of directory names expected under .agents/skills/
100
- * after installation. Each entry contains agent directory names AND/OR pack directory names.
101
- */
102
- const EXPECTED_AGENTS = {
103
- core: { agents: ['agent-strategist', 'agent-architect-dev', 'agent-quality', 'agent-orchestrator'], packDir: null },
104
- osint: { agents: ['agent-shadow'], packDir: null },
105
- maker: { agents: ['agent-maker'], packDir: null },
106
- shield: { agents: ['shield-orchestrator'], packDir: 'pack-shield' },
107
- seo: { agents: ['seo-scout', 'seo-chief', 'seo-judge'], packDir: 'pack-seo' },
108
- memory: { agents: ['zecher'], packDir: 'pack-memory' },
109
- 'dev-studio': { agents: ['dev-studio-orchestrator'], packDir: 'pack-dev-studio' },
110
- backup: { agents: ['backup-agent'], packDir: 'pack-backup' },
111
- animated: { agents: ['animated-website-agent'], packDir: 'pack-animated' },
112
- };
113
-
114
- module.exports = { PACKS, PACK_ORDER, EXPECTED_AGENTS };
1
+ /**
2
+ * BMAD+ Shared PACKS Module
3
+ * Single source of truth for pack definitions, expected agents, and pack ordering.
4
+ *
5
+ * Author: Laurent Rochetta
6
+ */
7
+
8
+ const PACKS = {
9
+ core: {
10
+ name: 'Core',
11
+ icon: 'b',
12
+ agents: ['agent-strategist', 'agent-architect-dev', 'agent-quality', 'agent-orchestrator'],
13
+ skills: ['bmad-plus-autopilot', 'bmad-plus-parallel', 'bmad-plus-sync'],
14
+ data: ['role-triggers.yaml'],
15
+ packDir: 'pack-core',
16
+ packSrcDir: 'packs',
17
+ required: true,
18
+ desc: 'Core agents & skills',
19
+ },
20
+ osint: {
21
+ name: 'OSINT',
22
+ icon: 'j',
23
+ agents: ['agent-shadow'],
24
+ skills: [],
25
+ externalPackage: 'osint-agent-package',
26
+ packDir: 'pack-osint',
27
+ packSrcDir: 'packs',
28
+ desc: 'OSINT & investigation',
29
+ },
30
+ maker: {
31
+ name: 'Maker',
32
+ icon: 'f',
33
+ agents: ['agent-maker'],
34
+ skills: [],
35
+ data: [],
36
+ packDir: 'pack-maker',
37
+ packSrcDir: 'packs',
38
+ desc: 'Agent creation toolkit',
39
+ },
40
+ shield: {
41
+ name: 'Shield',
42
+ icon: 'm',
43
+ agents: ['shield-orchestrator'],
44
+ skills: [],
45
+ packDir: 'pack-shield',
46
+ packSrcDir: 'packs',
47
+ desc: 'GRC compliance (25+ frameworks)',
48
+ },
49
+ seo: {
50
+ name: 'SEO',
51
+ icon: 'k',
52
+ agents: ['seo-scout', 'seo-chief', 'seo-judge'],
53
+ skills: [],
54
+ packDir: 'pack-seo',
55
+ packSrcDir: 'packs',
56
+ desc: 'SEO audit & optimization',
57
+ },
58
+ memory: {
59
+ name: 'Memory',
60
+ icon: 'x',
61
+ agents: ['zecher'],
62
+ skills: [],
63
+ packDir: 'pack-memory',
64
+ packSrcDir: 'packs',
65
+ desc: 'Persistent cross-session memory',
66
+ },
67
+ 'dev-studio': {
68
+ name: 'Dev Studio',
69
+ icon: 'v',
70
+ agents: ['dev-studio-orchestrator'],
71
+ skills: ['dev-studio'],
72
+ packDir: 'pack-dev-studio',
73
+ packSrcDir: 'packs',
74
+ desc: 'SDLC automation (6 agents, 56+ skills)',
75
+ },
76
+ backup: {
77
+ name: 'Backup',
78
+ icon: 'y',
79
+ agents: ['backup-agent'],
80
+ skills: [],
81
+ packDir: 'pack-backup',
82
+ packSrcDir: 'packs',
83
+ desc: 'Backup & restore',
84
+ },
85
+ animated: {
86
+ name: 'Animated',
87
+ icon: 'z',
88
+ agents: ['animated-website-agent'],
89
+ skills: [],
90
+ packDir: 'pack-animated',
91
+ packSrcDir: 'packs',
92
+ desc: 'Animated website agents',
93
+ },
94
+ };
95
+
96
+ const PACK_ORDER = ['core', 'osint', 'maker', 'shield', 'seo', 'memory', 'dev-studio', 'backup', 'animated'];
97
+
98
+ /**
99
+ * Maps each pack to what `bmad-plus doctor` should expect under .agents/skills/
100
+ * after installation.
101
+ *
102
+ * - `agents` — loose agent DIRECTORIES copied straight into .agents/skills/
103
+ * (only core/osint/maker ship their agents this way).
104
+ * - `packDir` — the pack DIRECTORY copied into .agents/skills/ (null when none).
105
+ * - `packAgents`— agent FILES that live INSIDE `packDir` (shield/seo/memory/
106
+ * dev-studio/backup/animated bundle their agents this way, so they
107
+ * must not be checked as loose directories — doing so produced
108
+ * false "Missing agent" warnings on every healthy install).
109
+ */
110
+ const EXPECTED_AGENTS = {
111
+ core: { agents: ['agent-strategist', 'agent-architect-dev', 'agent-quality', 'agent-orchestrator'], packDir: null },
112
+ osint: { agents: ['agent-shadow'], packDir: null },
113
+ maker: { agents: ['agent-maker'], packDir: null },
114
+ shield: { agents: [], packDir: 'pack-shield', packAgents: ['shield-orchestrator.md'] },
115
+ seo: { agents: [], packDir: 'pack-seo', packAgents: ['seo-scout.md', 'seo-chief.md', 'seo-judge.md'] },
116
+ memory: { agents: [], packDir: 'pack-memory', packAgents: ['zecher-agent.md', 'memory-orchestrator.md'] },
117
+ 'dev-studio': { agents: [], packDir: 'pack-dev-studio', packAgents: ['dev-studio-orchestrator.md'] },
118
+ backup: { agents: [], packDir: 'pack-backup', packAgents: ['backup-agent.md'] },
119
+ animated: { agents: [], packDir: 'pack-animated', packAgents: ['animated-website-agent.md'] },
120
+ };
121
+
122
+ module.exports = { PACKS, PACK_ORDER, EXPECTED_AGENTS };
@@ -3,8 +3,12 @@
3
3
  * Extracted from install.js for modularity and reuse.
4
4
  */
5
5
 
6
- // Shell metacharacters that are dangerous in user-provided names
7
- const SHELL_META = /[;&|`$(){}[\]!#~<>*?\\\n\r]/;
6
+ // Shell/quote metacharacters that are dangerous in user-provided names.
7
+ // Includes single/double quotes so a name cannot break out of a quoted context.
8
+ const SHELL_META = /[;&|`$(){}[\]!#~<>*?\\'"\n\r]/;
9
+ // Global variant used to strip EVERY occurrence during sanitization.
10
+ // (A non-global regex in String.replace only removes the first match.)
11
+ const SHELL_META_GLOBAL = /[;&|`$(){}[\]!#~<>*?\\'"\n\r]/g;
8
12
 
9
13
  /**
10
14
  * Validate and sanitize a user name.
@@ -31,7 +35,7 @@ function validateUserName(rawName, fallback) {
31
35
  }
32
36
 
33
37
  if (SHELL_META.test(rawName)) {
34
- const sanitized = rawName.replace(SHELL_META, '').trim() || 'Developer';
38
+ const sanitized = rawName.replace(SHELL_META_GLOBAL, '').trim() || 'Developer';
35
39
  warnings.push('Name contains shell metacharacters. Using sanitized version.');
36
40
  return { name: sanitized, warnings };
37
41
  }
@@ -42,4 +46,5 @@ function validateUserName(rawName, fallback) {
42
46
  module.exports = {
43
47
  validateUserName,
44
48
  SHELL_META,
49
+ SHELL_META_GLOBAL,
45
50
  };