@automagik/genie 4.260424.13 → 4.260424.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@automagik/genie",
3
- "version": "4.260424.13",
3
+ "version": "4.260424.14",
4
4
  "description": "Collaborative terminal toolkit for human + AI workflows",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie",
3
- "version": "4.260424.13",
3
+ "version": "4.260424.14",
4
4
  "description": "Human-AI partnership for Claude Code. Share a terminal, orchestrate workers, evolve together. Brainstorm ideas, turn them into wishes, execute with /work, validate with /review, and ship as one team.",
5
5
  "author": {
6
6
  "name": "Namastex Labs"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "genie-plugin",
3
- "version": "4.260424.13",
3
+ "version": "4.260424.14",
4
4
  "private": true,
5
5
  "description": "Runtime dependencies for genie bundled CLIs",
6
6
  "type": "module",
@@ -1379,6 +1379,34 @@ function findVersionsInText(text) {
1379
1379
  return found;
1380
1380
  }
1381
1381
 
1382
+ // Name-version correlated match. A tracked version string (e.g. "1.0.3")
1383
+ // may belong to MANY unrelated packages (tweetnacl@1.0.3, escape-html@1.0.3,
1384
+ // @inquirer/external-editor@1.0.3). findVersionsInText matches the bare
1385
+ // version and produces false positives on every unrelated package sharing
1386
+ // the same number. This variant only returns tuples `name@version` when
1387
+ // BOTH appear correlated in the text:
1388
+ // - Form 1: the literal "name@version" substring (bun.lock, yarn.lock,
1389
+ // pnpm-lock.yaml all use this).
1390
+ // - Form 2: JSON-style '"name" ... "version": "X.Y.Z"' within 500 chars
1391
+ // (package-lock.json, npm manifest).
1392
+ function findTrackedPackageVersionTuples(text) {
1393
+ if (!text) return [];
1394
+ const matches = new Set();
1395
+ for (const pkg of TRACKED_PACKAGES) {
1396
+ const escName = pkg.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
1397
+ for (const version of pkg.versions) {
1398
+ if (text.includes(`${pkg.name}@${version}`)) {
1399
+ matches.add(`${pkg.name}@${version}`);
1400
+ continue;
1401
+ }
1402
+ const escVersion = version.replace(/\./g, '\\.');
1403
+ const pattern = new RegExp(`"${escName}"[^{]{0,50}\\{[^{}]{0,500}"version"\\s*:\\s*"${escVersion}"`, 's');
1404
+ if (pattern.test(text)) matches.add(`${pkg.name}@${version}`);
1405
+ }
1406
+ }
1407
+ return [...matches];
1408
+ }
1409
+
1382
1410
  function collectTextIndicators(text) {
1383
1411
  const indicators = {
1384
1412
  versions: findVersionsInText(text),
@@ -1881,13 +1909,26 @@ function scanNpmCache(homePath, report) {
1881
1909
  }
1882
1910
  }
1883
1911
 
1884
- if (metadata.observedVersions.length > 0) {
1912
+ // Only flag when the cached npm manifest's dist-tag points at a
1913
+ // compromised version. The manifest ALWAYS lists every historical
1914
+ // version by design; that list alone is not evidence of compromise —
1915
+ // only "latest" or "next" resolving to a bad version means the user's
1916
+ // next `npm install <pkg>` would pull the malware. If a tarball
1917
+ // fetch for a compromised version is separately recorded, that goes
1918
+ // through npmTarballFetches as 'affected' severity regardless.
1919
+ const distTagsCompromised = metadata.distTags
1920
+ ? Object.entries(metadata.distTags).filter(([, v]) => TRACKED_VERSION_SET.has(v))
1921
+ : [];
1922
+ if (metadata.observedVersions.length > 0 && distTagsCompromised.length > 0) {
1923
+ metadata.compromisedDistTags = distTagsCompromised.map(([tag, version]) => ({ tag, version }));
1885
1924
  report.npmCacheMetadata.push(metadata);
1886
1925
  addTimeline(report, {
1887
1926
  time: metadata.observedAt || metadata.cacheRecordTime,
1888
1927
  category: 'npm-cache-metadata',
1889
1928
  severity: 'observed',
1890
- summary: `npm cache metadata recorded compromised versions ${metadata.observedVersions.join(', ')}`,
1929
+ summary: `npm cache dist-tag points at compromised version: ${distTagsCompromised
1930
+ .map(([tag, version]) => `${tag}=${version}`)
1931
+ .join(', ')}`,
1891
1932
  path: cacheRoot,
1892
1933
  });
1893
1934
  }
@@ -1928,20 +1969,20 @@ function scanNpmCache(homePath, report) {
1928
1969
  const text = safeReadText(fullPath);
1929
1970
  if (!text || !TRACKED_PACKAGES.some(({ name }) => text.includes(name))) continue;
1930
1971
 
1931
- const versions = findVersionsInText(text);
1972
+ // Name-version correlated match — the log may contain `1.0.3` because
1973
+ // an unrelated transitive dep (escape-html, tweetnacl) resolved to that
1974
+ // version during the same install. Only flag when the tracked name
1975
+ // and a tracked version appear as a correlated tuple.
1976
+ const versionTuples = findTrackedPackageVersionTuples(text);
1932
1977
  const indicators = collectTextIndicators(text);
1933
- // Hard evidence in an npm log: an actual compromised version string
1934
- // or IOC network pattern. Name-only install/exec entries happen every
1935
- // time anyone runs `npx @automagik/genie ...` and are NOT evidence of
1936
- // compromise — they just record that the package was interacted with.
1937
- const hardEvidence = versions.length > 0 || indicators.iocMatches.length > 0;
1978
+ const hardEvidence = versionTuples.length > 0 || indicators.iocMatches.length > 0;
1938
1979
  if (!hardEvidence) continue;
1939
1980
 
1940
1981
  report.npmLogHits.push({
1941
1982
  home: homePath,
1942
1983
  path: fullPath,
1943
1984
  modifiedAt: isoTime(safeStat(fullPath)?.mtimeMs),
1944
- versions,
1985
+ versions: versionTuples,
1945
1986
  installCommands: indicators.installCommands,
1946
1987
  executionCommands: indicators.executionCommands,
1947
1988
  iocMatches: indicators.iocMatches,
@@ -2231,21 +2272,23 @@ function scanProjectRoots(roots, report, runtime) {
2231
2272
 
2232
2273
  const text = safeReadText(lockfilePath);
2233
2274
  if (!text) return;
2234
- if (!TRACKED_PACKAGES.some(({ name }) => text.includes(name))) return;
2235
2275
 
2236
- const versions = findVersionsInText(text);
2237
- if (versions.length === 0) return;
2276
+ // Correlated tuples only — raw version match produced false positives
2277
+ // whenever an unrelated dep shared a version string with a tracked
2278
+ // compromised version (tweetnacl@1.0.3 vs @openwebconcept/design-tokens@1.0.3).
2279
+ const tuples = findTrackedPackageVersionTuples(text);
2280
+ if (tuples.length === 0) return;
2238
2281
 
2239
2282
  report.lockfileFindings.push({
2240
2283
  path: lockfilePath,
2241
2284
  modifiedAt: isoTime(stat.mtimeMs),
2242
- versions,
2285
+ versions: tuples,
2243
2286
  });
2244
2287
  addTimeline(report, {
2245
2288
  time: isoTime(stat.mtimeMs),
2246
2289
  category: 'lockfile',
2247
2290
  severity: 'observed',
2248
- summary: `lockfile references compromised versions ${versions.join(', ')}`,
2291
+ summary: `lockfile references compromised ${tuples.join(', ')}`,
2249
2292
  path: lockfilePath,
2250
2293
  });
2251
2294
  },
@@ -3023,9 +3066,14 @@ function summarize(report) {
3023
3066
  if (installHistoryEvidence > 0) {
3024
3067
  affectedReasons.push('shell history shows package installation commands');
3025
3068
  }
3026
- if (report.tempArtifactFindings.length > 0 && strongTempEvidence === 0) {
3027
- affectedReasons.push('temp or cache directories retain suspicious tarball or package references');
3028
- }
3069
+ // Weak temp findings (name-only matches on /tmp text files like Claude
3070
+ // session logs and bun task-output dumps) are NOT affected-grade evidence
3071
+ // — they're just text files that happen to contain the string "pgserve"
3072
+ // or "@automagik/genie". They still appear in the report for transparency
3073
+ // but must not elevate status from OBSERVED ONLY to LIKELY AFFECTED.
3074
+ // Only `strongTempEvidence` (IOC string, malware hash, env-compat name)
3075
+ // pushes a compromise/affected reason, via the `compromiseReasons` branch
3076
+ // above.
3029
3077
 
3030
3078
  const likelyCompromised = compromiseReasons.length > 0;
3031
3079
  const likelyAffected =