docguard-cli 0.21.0 → 0.21.1

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.
@@ -15,7 +15,7 @@ import { fileURLToPath } from 'node:url';
15
15
  import { createInterface } from 'node:readline';
16
16
  import { execSync } from 'node:child_process';
17
17
  import { c, PROFILES } from '../shared.mjs';
18
- import { ensureSkills, detectAgentMode, detectAIAgent, isSpecKitAvailable, isSpecKitInitialized, getDetectedAgent } from '../ensure-skills.mjs';
18
+ import { ensureSkills, detectAgentMode, detectAIAgent, isSpecKitAvailable, isSpecKitInitialized, getDetectedAgent, safeSpawnSpecify } from '../ensure-skills.mjs';
19
19
 
20
20
  // v0.20: scaffolder names that can be passed via `init --with <name>` and
21
21
  // dispatched to the corresponding standalone runner. Each name maps to its
@@ -378,17 +378,22 @@ poetry.lock
378
378
  } else if (specKitAvailable && !specKitInitialized) {
379
379
  console.log(`\n ${c.bold}🌱 Spec Kit Integration${c.reset}`);
380
380
 
381
- // Detect which AI agent is in use (matches spec-kit's --ai flag)
381
+ // Detect which AI agent is in use (matches spec-kit's --ai flag).
382
+ // v0.21.1 (issue #190): the returned value is allowlist-validated inside
383
+ // getDetectedAgent, so an attacker-controlled `.specify/init-options.json`
384
+ // can no longer inject shell metacharacters here.
382
385
  const detectedAgent = detectAIAgent(projectDir);
383
- const aiFlag = detectedAgent
384
- ? `--ai ${detectedAgent}`
385
- : '--ai generic --ai-commands-dir .agent/commands/';
386
+ const aiArgs = detectedAgent
387
+ ? ['--ai', detectedAgent]
388
+ : ['--ai', 'generic', '--ai-commands-dir', '.agent/commands/'];
386
389
 
387
390
  console.log(` ${c.dim}Running specify init (agent: ${detectedAgent || 'generic'})...${c.reset}`);
388
391
  try {
389
- const scriptFlag = process.platform === 'win32' ? '--script ps' : '--script sh';
390
- execSync(
391
- `specify init --here --force ${aiFlag} --ai-skills --ignore-agent-tools --no-git ${scriptFlag}`,
392
+ // v0.21.1 (issue #190): execFileSync via safeSpawnSpecify args pass
393
+ // through as an array, no shell interpolation.
394
+ const scriptArgs = process.platform === 'win32' ? ['--script', 'ps'] : ['--script', 'sh'];
395
+ safeSpawnSpecify(
396
+ ['init', '--here', '--force', ...aiArgs, '--ai-skills', '--ignore-agent-tools', '--no-git', ...scriptArgs],
392
397
  { cwd: projectDir, encoding: 'utf-8', stdio: 'pipe', timeout: 30000 }
393
398
  );
394
399
  console.log(` ${c.green}✅${c.reset} Spec Kit initialized ${c.dim}(.specify/, spec-kit skills, agent: ${detectedAgent || 'generic'})${c.reset}`);
@@ -13,9 +13,32 @@
13
13
  import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
14
14
  import { resolve, dirname } from 'node:path';
15
15
  import { fileURLToPath } from 'node:url';
16
- import { execSync } from 'node:child_process';
16
+ import { execSync, execFileSync } from 'node:child_process';
17
17
  import { c } from './shared.mjs';
18
18
 
19
+ /**
20
+ * v0.21.1 (security): cross-platform safe spawn for the `specify` CLI.
21
+ *
22
+ * On POSIX, runs the `specify` binary directly with argv passed as an array
23
+ * — no shell interpolation possible. On Windows, the equivalent is via
24
+ * `cmd.exe /c specify.cmd ...` since `specify` is shipped as a .cmd shim by
25
+ * `pip install`. Args are still passed as an array so cmd.exe doesn't
26
+ * re-parse them.
27
+ *
28
+ * Replaces the pre-v0.21.1 pattern of `execSync(\`specify init ... \${flag} ...\`)`
29
+ * which was shell-interpolated and vulnerable to command injection via
30
+ * `.specify/init-options.json`'s `ai` field (issue #190).
31
+ */
32
+ export function safeSpawnSpecify(args, opts) {
33
+ if (!Array.isArray(args)) {
34
+ throw new TypeError('safeSpawnSpecify(args, opts): args must be an array');
35
+ }
36
+ if (process.platform === 'win32') {
37
+ return execFileSync('cmd.exe', ['/c', 'specify.cmd', ...args], opts);
38
+ }
39
+ return execFileSync('specify', args, opts);
40
+ }
41
+
19
42
  const __filename = fileURLToPath(import.meta.url);
20
43
  const __dirname = dirname(__filename);
21
44
 
@@ -77,12 +100,27 @@ export function detectAgentMode(projectDir) {
77
100
  * @param {string} projectDir - The project root directory
78
101
  * @returns {string | null}
79
102
  */
103
+ // v0.21.1 (security): allowlist for the spec-kit --ai flag value. Source
104
+ // values come from `.specify/init-options.json` which is attacker-writable
105
+ // in any compromised project. Without this filter, a value like
106
+ // `"claude; touch /tmp/pwned;"` would shell-execute on every `docguard init`.
107
+ //
108
+ // Set conservatively from spec-kit's published agent list. New agents
109
+ // require a code change to be accepted — by design.
110
+ const VALID_AI_AGENT = /^[a-zA-Z0-9_-]{1,32}$/;
111
+
80
112
  export function getDetectedAgent(projectDir) {
81
113
  const initOptions = resolve(projectDir, '.specify', 'init-options.json');
82
114
  if (existsSync(initOptions)) {
83
115
  try {
84
116
  const opts = JSON.parse(readFileSync(initOptions, 'utf-8'));
85
- return opts.ai || null;
117
+ const ai = opts.ai;
118
+ if (typeof ai !== 'string') return null;
119
+ // v0.21.1 (issue #190): reject anything outside the allowlist. Without
120
+ // this, a malicious `.specify/init-options.json` could inject shell
121
+ // metacharacters through to the `specify init` exec call.
122
+ if (!VALID_AI_AGENT.test(ai)) return null;
123
+ return ai;
86
124
  } catch { /* ignore */ }
87
125
  }
88
126
  return null;
@@ -193,13 +231,17 @@ export function ensureSpecKit(projectDir, flags = {}) {
193
231
  console.log(` ${c.cyan}🌱 Spec Kit detected — auto-initializing SDD workflow...${c.reset}`);
194
232
  }
195
233
  try {
234
+ // v0.21.1 (issue #190): switched from shell-interpolated execSync to
235
+ // execFileSync via safeSpawnSpecify. detectAIAgent now also enforces
236
+ // the [a-zA-Z0-9_-]{1,32} allowlist on values read from .specify/
237
+ // init-options.json — defense in depth.
196
238
  const detectedAgent = detectAIAgent(projectDir);
197
- const aiFlag = detectedAgent
198
- ? `--ai ${detectedAgent}`
199
- : '--ai generic --ai-commands-dir .agent/commands/';
200
- const scriptFlag = process.platform === 'win32' ? '--script ps' : '--script sh';
201
- execSync(
202
- `specify init --here --force ${aiFlag} --ai-skills --ignore-agent-tools --no-git ${scriptFlag}`,
239
+ const aiArgs = detectedAgent
240
+ ? ['--ai', detectedAgent]
241
+ : ['--ai', 'generic', '--ai-commands-dir', '.agent/commands/'];
242
+ const scriptArgs = process.platform === 'win32' ? ['--script', 'ps'] : ['--script', 'sh'];
243
+ safeSpawnSpecify(
244
+ ['init', '--here', '--force', ...aiArgs, '--ai-skills', '--ignore-agent-tools', '--no-git', ...scriptArgs],
203
245
  { cwd: projectDir, encoding: 'utf-8', stdio: 'pipe', timeout: 30000 }
204
246
  );
205
247
  if (!silent) {
@@ -3,7 +3,7 @@ schema_version: "1.0"
3
3
  extension:
4
4
  id: "docguard"
5
5
  name: "DocGuard — CDD Enforcement"
6
- version: "0.21.0"
6
+ version: "0.21.1"
7
7
  description: "Canonical-Driven Development enforcement as a true spec-kit extension. LLM-first design with 19 automated validators, 4 AI behavior skills, spec-kit skill chaining, and workflow hooks. Zero NPM runtime dependencies."
8
8
  author: "Ricardo Accioly"
9
9
  repository: "https://github.com/raccioly/docguard"
@@ -6,10 +6,10 @@ description: AI-driven documentation repair with structured research workflow, t
6
6
  compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
7
7
  metadata:
8
8
  author: docguard
9
- version: 0.21.0
9
+ version: 0.21.1
10
10
  source: extensions/spec-kit-docguard/skills/docguard-fix
11
11
  ---
12
- <!-- docguard:version: 0.21.0 -->
12
+ <!-- docguard:version: 0.21.1 -->
13
13
 
14
14
  # DocGuard Fix Skill
15
15
 
@@ -7,10 +7,10 @@ description: Run DocGuard guard validation against Canonical-Driven Development
7
7
  compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
8
8
  metadata:
9
9
  author: docguard
10
- version: 0.21.0
10
+ version: 0.21.1
11
11
  source: extensions/spec-kit-docguard/skills/docguard-guard
12
12
  ---
13
- <!-- docguard:version: 0.21.0 -->
13
+ <!-- docguard:version: 0.21.1 -->
14
14
 
15
15
  # DocGuard Guard Skill
16
16
 
@@ -6,10 +6,10 @@ description: Cross-document consistency analysis and quality assessment. Perform
6
6
  compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
7
7
  metadata:
8
8
  author: docguard
9
- version: 0.21.0
9
+ version: 0.21.1
10
10
  source: extensions/spec-kit-docguard/skills/docguard-review
11
11
  ---
12
- <!-- docguard:version: 0.21.0 -->
12
+ <!-- docguard:version: 0.21.1 -->
13
13
 
14
14
  # DocGuard Review Skill
15
15
 
@@ -6,10 +6,10 @@ description: CDD maturity assessment with category-aware improvement roadmap. Ru
6
6
  compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
7
7
  metadata:
8
8
  author: docguard
9
- version: 0.21.0
9
+ version: 0.21.1
10
10
  source: extensions/spec-kit-docguard/skills/docguard-score
11
11
  ---
12
- <!-- docguard:version: 0.21.0 -->
12
+ <!-- docguard:version: 0.21.1 -->
13
13
 
14
14
  # DocGuard Score Skill
15
15
 
@@ -4,7 +4,7 @@ description: Keep canonical documentation ALWAYS UP TO DATE. Refreshes code-trut
4
4
  compatibility: Requires DocGuard CLI installed (npm i -g docguard-cli or npx docguard-cli)
5
5
  metadata:
6
6
  author: docguard
7
- version: 0.21.0
7
+ version: 0.21.1
8
8
  source: extensions/spec-kit-docguard/skills/docguard-sync
9
9
  ---
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "docguard-cli",
3
- "version": "0.21.0",
3
+ "version": "0.21.1",
4
4
  "description": "The enforcement tool for Canonical-Driven Development (CDD). Audit, generate, and guard your project documentation.",
5
5
  "type": "module",
6
6
  "bin": {