Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/workflows/server-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,3 +55,10 @@ jobs:
run: npx playwright install chromium --with-deps
- name: Cloud UI e2e
run: npm run test:e2e:cloud
- name: CSV formula injection fuzz e2e
# Runs tests/e2e/csv_formula_fuzz.spec.js as a required job. It used to be
# chained off `npm run coverage`, which only the central coverage-evidence
# sandbox invokes (and there Playwright is absent), so the CSV formula
# injection defense never actually ran in CI. Run it here where Playwright
# and the static webServer are available.
run: npm run test:fuzz
30 changes: 30 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Documented Kubernetes/IaC as follow-up work rather than a current
blocker for this static app.

### Security

- Removed the dynamic `new RegExp(...)` in `parseMsProjectXml`'s `tag()` helper
(`cloud-sync.js`) that the SAST gate flagged for potential ReDoS
(`detect-non-literal-regexp`). Every caller passes a hardcoded tag name and
the pattern was linear, so it was not exploitable, but the extractor now uses
`indexOf` slicing with identical semantics — clearing the finding at base
rather than suppressing it. Verified unchanged by the MS Project import unit
tests.
- Upgraded the `@hono/node-server` runtime dependency from `^1.19.14` to
`^2.0.12` to clear GHSA-frvp-7c67-39w9 (moderate: `serve-static` path
traversal on Windows via an encoded backslash). ScopeWeave only calls the
stable `serve()` entry — it does not use `@hono/node-server`'s `serveStatic`
helper, so the vulnerable path was unreachable, but the dependency is bumped
to keep the lockfiles clean. Verified with `npm run test:unit` and
`npm run test:api` (server boots and passes the API + rate-limit smoke under
v2); `package-lock.json` updated.

### Removed

- Removed the vestigial `pnpm-lock.yaml`. ScopeWeave is an npm project
(`package-lock.json` is canonical; `server-tests.yml`/`fuzz.yml` use
`cache: 'npm'` + `npm ci`; no workflow, Dockerfile, or compose file
references pnpm), so the committed pnpm lock was a second, unused lockfile.
The central OpenCode coverage-evidence runner selects pnpm whenever a
`pnpm-lock.yaml` is present and then refuses because `package.json` declares
no exact `packageManager: pnpm@X.Y.Z`, which blocked coverage evidence — and
therefore review approval — on every ScopeWeave PR. Deleting the vestigial
lock lets the runner use the canonical npm path.

## [1.0.0] - 2026-04-20

<!-- markdownlint-disable-next-line MD024 -->
Expand Down
15 changes: 13 additions & 2 deletions cloud-sync.js
Original file line number Diff line number Diff line change
Expand Up @@ -739,9 +739,20 @@ function openReportModal() {
// no DOMParser needed → node-testable); swap for a real XML parser if
// hand-edited files ever matter.
export function parseMsProjectXml(xml) {
// Extract the text of a simple <name>…</name> element without building a
// dynamic RegExp from `name` (which trips SAST ReDoS/regex-injection rules,
// even though every caller passes a hardcoded tag). indexOf slicing mirrors
// the original /<name>([^<]*)<\/name>/ semantics: the run must contain no
// '<', so a nested tag yields '' rather than a false capture.
const tag = (block, name) => {
const m = block.match(new RegExp(`<${name}>([^<]*)</${name}>`));
return m ? m[1].trim() : '';
const open = `<${name}>`;
const start = block.indexOf(open);
if (start === -1) return '';
const from = start + open.length;
const end = block.indexOf(`</${name}>`, from);
if (end === -1) return '';
const content = block.slice(from, end);
return content.includes('<') ? '' : content.trim();
};
const unescape = (s) => s
.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&quot;/g, '"')
Expand Down
10 changes: 5 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"description": "Production-grade pure HTML/CSS/JS WBS planner",
"scripts": {
"check:python-docstrings": "node scripts/ci/static_coverage_evidence.mjs docstrings",
"coverage": "node scripts/ci/static_coverage_evidence.mjs coverage && npm run test:fuzz",
"coverage": "node scripts/ci/static_coverage_evidence.mjs coverage",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

rg -n '"test:fuzz"|npm run test:fuzz' package.json .github scripts

Repository: ContextualWisdomLab/scopeweave

Length of output: 286


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n## package.json\n'
cat -n package.json | sed -n '1,120p'

printf '\n## workflow references\n'
rg -n '"test:fuzz"|npm run test:fuzz|test:fuzz' .github package.json scripts -g '!**/node_modules/**'

Repository: ContextualWisdomLab/scopeweave

Length of output: 1971


test:fuzz를 별도 필수 CI 단계로 유지하세요. coverage는 이제 정적 커버리지 증거만 생성하므로 tests/e2e/csv_formula_fuzz.spec.js가 함께 실행되지 않습니다. CSV 수식 주입 방어가 빠지지 않도록 이 퍼즈 테스트를 독립적인 required job으로 돌려야 합니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@package.json` at line 9, coverage 스크립트와 분리하여 test:fuzz를 독립적인 필수 CI job으로
유지하세요. test:fuzz가 csv_formula_fuzz.spec.js를 직접 실행하도록 구성하고, 정적 커버리지 증거 생성에 의존하지
않게 하세요. CI 설정에서 해당 job이 required 상태로 적용되는지 확인하세요.

"server": "node server/server.mjs",
"test:api": "node tests/api/smoke.mjs && node tests/api/ratelimit.test.mjs",
"test:unit": "node tests/unit/analytics.test.mjs && node tests/unit/cpm.test.mjs && node tests/unit/baseline-compare.test.mjs && node tests/unit/workload.test.mjs && node tests/unit/cost-evm.test.mjs && node tests/unit/msproject.test.mjs && node tests/unit/dep-types.test.mjs && node tests/unit/weekly-report.test.mjs && node tests/unit/clearfolio.test.mjs && node tests/unit/sprint-stats.test.mjs && node tests/unit/burndown.test.mjs && node tests/unit/pm-analysis.test.mjs && node tests/unit/cloud-sync-security.test.mjs",
Expand All @@ -17,7 +17,7 @@
"fuzz": "node --test tests/fuzz/*.mjs"
},
"dependencies": {
"@hono/node-server": "^1.19.14",
"@hono/node-server": "^2.0.12",
"hono": "^4.12.27"
},
"devDependencies": {
Expand Down
91 changes: 0 additions & 91 deletions pnpm-lock.yaml

This file was deleted.

29 changes: 29 additions & 0 deletions tests/unit/msproject.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,33 @@ assert.equal(t5.predecessors, 'msp-1');

assert.deepEqual(parseMsProjectXml('<Project></Project>'), [], 'no tasks → empty');

// --- tag() failure-branch regression tests (indexOf-slicing extractor) ---
// These pin the three defensive branches of the `tag()` helper introduced when
// the dynamic RegExp was removed, so a future parser change cannot silently
// alter how malformed or nested markup is handled.

// end === -1: <Name> opens but never closes → '' → task has no usable name → skipped.
assert.deepEqual(
parseMsProjectXml('<Task><UID>10</UID><Name>broken</Task>'),
[],
'unterminated <Name> (end === -1) yields no name → task skipped',
);

// content.includes('<'): nested markup inside the value → '' → task skipped
// (mirrors the original /<name>([^<]*)<\/name>/ "no inner <" semantics).
assert.deepEqual(
parseMsProjectXml('<Task><UID>11</UID><Name>a<b>c</b></Name><OutlineLevel>1</OutlineLevel></Task>'),
[],
'nested markup inside <Name> (content contains "<") is rejected → task skipped',
);

// start === -1: absent optional tags → '' (dates blank, OutlineLevel defaults to 1).
const sparseTasks = parseMsProjectXml('<Task><UID>12</UID><Name>NoDates</Name></Task>');
assert.equal(sparseTasks.length, 1, 'valid UID + Name parses even with no dates/level');
assert.equal(sparseTasks[0].id, 'msp-12');
assert.equal(sparseTasks[0].depth, 1, 'missing <OutlineLevel> (start === -1) defaults depth to 1');
assert.equal(sparseTasks[0].plannedStartDate, '', 'missing <Start> (start === -1) → empty date');
assert.equal(sparseTasks[0].plannedEndDate, '', 'missing <Finish> (start === -1) → empty date');
assert.equal(sparseTasks[0].actualProgress, 0, 'missing <PercentComplete> → 0');

console.log('✓ MS Project import tests passed');
Loading