Skip to content
Closed
Show file tree
Hide file tree
Changes from 4 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
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.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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