flowcollab 0.3.8 → 0.3.9
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/bin/_client.mjs +15 -2
- package/bin/decisions.mjs +3 -3
- package/bin/scan.mjs +18 -6
- package/package.json +1 -1
package/bin/_client.mjs
CHANGED
|
@@ -123,6 +123,19 @@ export function die(msg, code = 1) {
|
|
|
123
123
|
process.exit(code);
|
|
124
124
|
}
|
|
125
125
|
|
|
126
|
+
// S14: strip ANSI / control chars and @to: tokens from server-supplied text (task
|
|
127
|
+
// titles, decision titles) before printing — they reach another agent's trusted stdout.
|
|
128
|
+
export function sanitizeText(s, maxLen = 120) {
|
|
129
|
+
if (!s || typeof s !== 'string') return '';
|
|
130
|
+
return s
|
|
131
|
+
.replace(/\x1b\[[0-9;]*[A-Za-z]/g, '')
|
|
132
|
+
.replace(/[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/g, '')
|
|
133
|
+
.replace(/@to:\S+/g, '[mention-stripped]')
|
|
134
|
+
.replace(/\r?\n|\r/g, ' ')
|
|
135
|
+
.trim()
|
|
136
|
+
.slice(0, maxLen);
|
|
137
|
+
}
|
|
138
|
+
|
|
126
139
|
export function arg(name, fallback) {
|
|
127
140
|
const argv = process.argv.slice(2);
|
|
128
141
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -157,10 +170,10 @@ export async function resolveTaskId(input) {
|
|
|
157
170
|
const decRes = await flowFetch('/api/flow/decisions');
|
|
158
171
|
const taskMatches = allTasks.filter(t => typeof t.id === 'string' && t.id.startsWith(stripped));
|
|
159
172
|
if (taskMatches.length === 1) return taskMatches[0].id;
|
|
160
|
-
if (taskMatches.length > 1) throw new Error(`Ambiguous prefix "${stripped}" matches ${taskMatches.length} tasks:\n${taskMatches.map(t => ` #${t.id.slice(0, 6)} ${t.title}`).join('\n')}`);
|
|
173
|
+
if (taskMatches.length > 1) throw new Error(`Ambiguous prefix "${stripped}" matches ${taskMatches.length} tasks:\n${taskMatches.map(t => ` #${t.id.slice(0, 6)} ${sanitizeText(t.title)}`).join('\n')}`);
|
|
161
174
|
const decisions = decRes.decisions || [];
|
|
162
175
|
const decMatches = decisions.filter(d => d.id.startsWith(stripped));
|
|
163
176
|
if (decMatches.length === 0) throw new Error(`No task or decision found with ID prefix "${stripped}". Run \`flow-pull\` to see your current tasks.`);
|
|
164
|
-
if (decMatches.length > 1) throw new Error(`Ambiguous prefix "${stripped}" matches ${decMatches.length} decisions:\n${decMatches.map(d => ` #${d.id.slice(0, 6)} ${d.proposal_title || ''}`).join('\n')}`);
|
|
177
|
+
if (decMatches.length > 1) throw new Error(`Ambiguous prefix "${stripped}" matches ${decMatches.length} decisions:\n${decMatches.map(d => ` #${d.id.slice(0, 6)} ${sanitizeText(d.proposal_title || '')}`).join('\n')}`);
|
|
165
178
|
return decMatches[0].id;
|
|
166
179
|
}
|
package/bin/decisions.mjs
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
--timeout=<sec> give up after N seconds, default 1800 (30 min); 0 = forever
|
|
20
20
|
*/
|
|
21
21
|
|
|
22
|
-
import { flowFetch, resolveTaskId, arg, positional, die } from './_client.mjs';
|
|
22
|
+
import { flowFetch, resolveTaskId, arg, positional, die, sanitizeText } from './_client.mjs';
|
|
23
23
|
|
|
24
24
|
const sleep = (ms) => new Promise(r => setTimeout(r, ms));
|
|
25
25
|
|
|
@@ -36,10 +36,10 @@ async function list() {
|
|
|
36
36
|
}
|
|
37
37
|
process.stdout.write(`${decisions.length} pending decision${decisions.length === 1 ? '' : 's'}:\n\n`);
|
|
38
38
|
for (const d of decisions) {
|
|
39
|
-
process.stdout.write(`#${d.id.slice(0, 6)} ${d.proposal_title}\n`);
|
|
39
|
+
process.stdout.write(`#${d.id.slice(0, 6)} ${sanitizeText(d.proposal_title)}\n`);
|
|
40
40
|
if (d.parent_task_id) {
|
|
41
41
|
const p = taskById.get(d.parent_task_id);
|
|
42
|
-
const ref = p ? `#${p.id.slice(0, 6)} — "${p.title}"` : `#${d.parent_task_id.slice(0, 6)}`;
|
|
42
|
+
const ref = p ? `#${p.id.slice(0, 6)} — "${sanitizeText(p.title)}"` : `#${d.parent_task_id.slice(0, 6)}`;
|
|
43
43
|
process.stdout.write(` Parent: ${ref}\n`);
|
|
44
44
|
}
|
|
45
45
|
process.stdout.write(` Confidence: ${d.confidence} | Suggest: ${d.suggested_assignee_id || '-'} / ${d.suggested_priority || '-'} / ${d.suggested_type || '-'}\n`);
|
package/bin/scan.mjs
CHANGED
|
@@ -19,16 +19,23 @@ import { join, extname, relative } from 'node:path';
|
|
|
19
19
|
const SKIP_DIRS = new Set(['node_modules', '.git', 'dist', 'build', 'coverage', '.next', '__pycache__', 'vendor', '.cache', 'out']);
|
|
20
20
|
const CODE_EXTS = new Set(['.js', '.mjs', '.ts', '.tsx', '.jsx', '.py', '.go', '.rb', '.java', '.sh', '.rs', '.c', '.cpp', '.h', '.cs', '.php', '.vue', '.svelte']);
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// S13: the security scan also looks at config/secret files (where keys actually live).
|
|
23
|
+
const SEC_EXTS = new Set([...CODE_EXTS, '.json', '.yml', '.yaml', '.toml', '.ini', '.cfg', '.conf', '.properties', '.xml', '.pem', '.key']);
|
|
24
|
+
function secMatch(name) {
|
|
25
|
+
return SEC_EXTS.has(extname(name)) || name.startsWith('.env') || name === '.npmrc' || name === '.netrc';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function walkFiles(dir, match, base) {
|
|
23
29
|
base = base || dir;
|
|
30
|
+
match = match || ((name) => CODE_EXTS.has(extname(name)));
|
|
24
31
|
const out = [];
|
|
25
32
|
let entries;
|
|
26
33
|
try { entries = readdirSync(dir, { withFileTypes: true }); } catch { return out; }
|
|
27
34
|
for (const e of entries) {
|
|
28
35
|
if (SKIP_DIRS.has(e.name)) continue;
|
|
29
36
|
const full = join(dir, e.name);
|
|
30
|
-
if (e.isDirectory()) { out.push(...walkFiles(full, base)); continue; }
|
|
31
|
-
if (
|
|
37
|
+
if (e.isDirectory()) { out.push(...walkFiles(full, match, base)); continue; }
|
|
38
|
+
if (match(e.name)) out.push(full);
|
|
32
39
|
}
|
|
33
40
|
return out;
|
|
34
41
|
}
|
|
@@ -140,6 +147,10 @@ async function scanPRs(repo) {
|
|
|
140
147
|
// ── Security scan ──────────────────────────────────────────────────────────────
|
|
141
148
|
const SEC_PATTERNS = [
|
|
142
149
|
{ re: /(['"`])(sk_live_|AKIA[0-9A-Z]{16}|ghp_[0-9A-Za-z]{36}|github_pat_[0-9A-Za-z]{36}|glpat-[0-9A-Za-z]{20}|xox[bpas]-[0-9A-Za-z-]{10,})[0-9A-Za-z-]*\1/i, label: 'Hardcoded secret (live key pattern)' },
|
|
150
|
+
// S13: same prefixes but UNQUOTED (e.g. KEY=value in .env), variable length
|
|
151
|
+
{ re: /\b(sk_(?:live|test)_[0-9A-Za-z]{10,}|whsec_[0-9A-Za-z]{10,}|ghp_[0-9A-Za-z]{20,}|github_pat_[0-9A-Za-z_]{20,}|glpat-[0-9A-Za-z_-]{15,}|xox[bpas]-[0-9A-Za-z-]{10,}|AKIA[0-9A-Z]{16})\b/, label: 'Secret token (unquoted key prefix)' },
|
|
152
|
+
// S13: env-style assignment of a secret-named variable (KEY=value, no quotes needed)
|
|
153
|
+
{ re: /(?:^|\b)[A-Za-z][A-Za-z0-9_]*(?:SECRET|TOKEN|PASSWORD|PASSWD|API_?KEY|PRIVATE_KEY|ACCESS_KEY)\s*=\s*\S{6,}/i, label: 'Secret in env-style assignment' },
|
|
143
154
|
{ re: /\beval\s*\(/, label: 'eval() usage (code injection risk)' },
|
|
144
155
|
{ re: /dangerouslySetInnerHTML/, label: 'dangerouslySetInnerHTML (XSS risk)' },
|
|
145
156
|
{ re: /(?:child_process.*\.(?:exec|execSync|spawn|spawnSync)\s*\(|execSync\s*\(|require\(['"]child_process['"]\))/, label: 'exec/spawn call (command injection risk)' },
|
|
@@ -150,7 +161,8 @@ const SEC_PATTERNS = [
|
|
|
150
161
|
{ re: /(?:password|secret|apikey|api_key|access_token)\s*(?:=|:)\s*['"`][^'"`]{6,}/i, label: 'Possible hardcoded credential' },
|
|
151
162
|
];
|
|
152
163
|
|
|
153
|
-
function scanSecurity(
|
|
164
|
+
function scanSecurity(dir) {
|
|
165
|
+
const files = walkFiles(dir, secMatch); // S13: include config/.env/secret files
|
|
154
166
|
const out = [];
|
|
155
167
|
for (const { re, label } of SEC_PATTERNS) {
|
|
156
168
|
const hits = grepLines(files, re);
|
|
@@ -194,7 +206,7 @@ async function main() {
|
|
|
194
206
|
if (repo) process.stdout.write(` repo: ${repo}\n`);
|
|
195
207
|
process.stdout.write('\n');
|
|
196
208
|
|
|
197
|
-
const needFiles = all || doTodos
|
|
209
|
+
const needFiles = all || doTodos; // security walks its own (broader) file set
|
|
198
210
|
const files = needFiles ? walkFiles(dir) : [];
|
|
199
211
|
if (needFiles) process.stdout.write(` scanning ${files.length} source files...\n`);
|
|
200
212
|
|
|
@@ -230,7 +242,7 @@ async function main() {
|
|
|
230
242
|
|
|
231
243
|
if (all || doSecurity) {
|
|
232
244
|
process.stdout.write(' [security] scanning patterns...\n');
|
|
233
|
-
const s = scanSecurity(
|
|
245
|
+
const s = scanSecurity(dir);
|
|
234
246
|
if (doSecurity) { printSection('Security concerns', s); return; }
|
|
235
247
|
if (all && s.length) printSection('Security concerns', s);
|
|
236
248
|
else if (all) process.stdout.write('\n[security] No common patterns found.\n');
|