tewip-ai 0.18.3 → 0.18.5

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.
@@ -5,6 +5,7 @@
5
5
  import { existsSync } from 'node:fs';
6
6
  import { join } from 'node:path';
7
7
  import { spawnSync } from 'node:child_process';
8
+ const NEWLINE = String.fromCharCode(10);
8
9
  const run = (command, cwd, timeoutMs = 5 * 60_000) => {
9
10
  const r = spawnSync(command, { cwd, shell: true, encoding: 'utf8', timeout: timeoutMs, maxBuffer: 40 * 1024 * 1024 });
10
11
  return { code: r.status ?? 1, out: `${r.stdout ?? ''}`, err: `${r.stderr ?? ''}` };
@@ -15,14 +16,26 @@ export const bySeverity = (a, b) => SEVERITY_ORDER.indexOf(a.severity.toLowerCas
15
16
  /** `npm audit --json`, which needs no install and no account: every Node project already has it. */
16
17
  export function npmAudit(root) {
17
18
  if (!existsSync(join(root, 'package.json')))
18
- return [];
19
- const { out } = run('npm audit --json', root);
19
+ return { findings: [], problem: null };
20
+ const { out, err } = run('npm audit --json', root);
20
21
  let report;
21
22
  try {
22
23
  report = JSON.parse(out);
23
24
  }
24
25
  catch {
25
- return [];
26
+ return { findings: [], problem: `npm audit did not return a report${err.trim() ? `: ${err.trim().split(NEWLINE)[0].slice(0, 120)}` : ''}` };
27
+ }
28
+ // npm reports a refusal inside the JSON and still exits 0: no lockfile, a registry that will not
29
+ // answer, a private registry needing credentials. None of those is "nothing found".
30
+ // Two shapes, both exiting 0. A refusal names itself - {"error":{"code":"ENOLOCK","summary":...}}
31
+ // - while a registry that cannot be reached leaves those fields empty and puts the reason in
32
+ // `message`. Reading summary with ?? kept the empty string, so an unreachable registry passed for
33
+ // a clean audit on a project with three known advisories.
34
+ const failure = report.error || (report.message && !report.vulnerabilities);
35
+ if (failure) {
36
+ const reason = [report.error?.summary, report.error?.code, report.message, 'npm audit could not run'].find((x) => typeof x === 'string' && x.trim());
37
+ const hint = report.error?.code === 'ENOLOCK' ? ' (run `npm i --package-lock-only` first)' : '';
38
+ return { findings: [], problem: `${String(reason).slice(0, 160)}${hint}` };
26
39
  }
27
40
  const findings = [];
28
41
  for (const [name, entry] of Object.entries(report.vulnerabilities ?? {})) {
@@ -34,11 +47,13 @@ export function npmAudit(root) {
34
47
  id: String(via?.url?.split('/').pop() ?? via?.source ?? ''),
35
48
  title: String(via?.title ?? `${name} has a known vulnerability`),
36
49
  fixedIn: entry.fixAvailable && typeof entry.fixAvailable === 'object' ? String(entry.fixAvailable.version ?? '') : '',
50
+ fixExists: entry.fixAvailable === true || (entry.fixAvailable && typeof entry.fixAvailable === 'object'),
51
+ direct: entry.isDirect === true,
37
52
  source: 'npm audit',
38
53
  dev: Boolean(entry.isDirect === false ? false : entry.dev ?? false) || Boolean(entry.effects?.length === 0 && entry.dev),
39
54
  });
40
55
  }
41
- return findings;
56
+ return { findings, problem: null };
42
57
  }
43
58
  /** `pip-audit`, when the project is Python and the tool is installed. */
44
59
  export function pipAudit(root) {
@@ -82,6 +97,10 @@ export function osvScan(root) {
82
97
  id: String(v.id ?? ''),
83
98
  title: String(v.summary ?? '').slice(0, 200),
84
99
  fixedIn: '',
100
+ // osv-scanner's output says neither which version fixes it nor whether the project
101
+ // declares the package, so neither is claimed.
102
+ fixExists: false,
103
+ direct: false,
85
104
  source: 'osv-scanner',
86
105
  dev: false,
87
106
  });
@@ -99,8 +118,12 @@ export function auditDependencies(root) {
99
118
  const missing = [];
100
119
  const findings = [];
101
120
  if (existsSync(join(root, 'package.json'))) {
102
- ran.push('npm audit');
103
- findings.push(...npmAudit(root));
121
+ const npm = npmAudit(root);
122
+ if (npm.problem)
123
+ missing.push(`npm audit could not run: ${npm.problem}`);
124
+ else
125
+ ran.push('npm audit');
126
+ findings.push(...npm.findings);
104
127
  }
105
128
  const python = ['requirements.txt', 'pyproject.toml', 'setup.py'].some((f) => existsSync(join(root, f)));
106
129
  if (python) {
@@ -116,7 +116,15 @@ export function triageDependency(finding, root, sources, accepted) {
116
116
  where,
117
117
  source: finding.source,
118
118
  };
119
- const upgrade = finding.fixedIn ? `upgrade ${finding.package} to ${finding.fixedIn}` : `no fixed version is published yet: watch ${finding.id || 'the advisory'} and consider replacing the package`;
119
+ // npm answers `fixAvailable: true` when `npm audit fix` can resolve something without naming a
120
+ // version. Reading only the version form made Tewip say "no fixed version is published yet,
121
+ // consider replacing the package" about three packages npm could have fixed in one command -
122
+ // found by running this against real advisories for lodash, minimist and marked.
123
+ const upgrade = finding.fixedIn
124
+ ? `upgrade ${finding.package} to ${finding.fixedIn}`
125
+ : finding.fixExists
126
+ ? `a fix is available: run \`npm audit fix\` (the scanner did not name a version)`
127
+ : `no fixed version is published yet: watch ${finding.id || 'the advisory'} and consider replacing the package`;
120
128
  if (accepted.has(`dependency ${finding.package}`) || accepted.has(`dependency ${where}`)) {
121
129
  return { ...base, verdict: 'accepted', why: 'a person marked this one accepted in .tewip/security-accepted.txt', next: 'nothing: it was reviewed' };
122
130
  }
@@ -134,8 +142,12 @@ export function triageDependency(finding, root, sources, accepted) {
134
142
  ...base,
135
143
  verdict: 'not-reachable',
136
144
  severity: base.severity === 'critical' ? 'medium' : 'low',
137
- why: `nothing in this project imports ${finding.package}: it is pulled in by another package, so the vulnerable code may never run here`,
138
- next: `${upgrade}, or check which dependency requires it (npm why ${finding.package}) before spending time on it`,
145
+ why: finding.direct
146
+ ? `this project declares ${finding.package} but no file imports it, so the vulnerable code may never run here`
147
+ : `nothing in this project imports ${finding.package}: it is pulled in by another package, so the vulnerable code may never run here`,
148
+ next: finding.direct
149
+ ? `${upgrade}, or remove ${finding.package} from the project if nothing uses it any more`
150
+ : `${upgrade}, or check which dependency requires it (npm why ${finding.package}) before spending time on it`,
139
151
  };
140
152
  }
141
153
  return { ...base, verdict: 'real-risk', why: `this project imports ${finding.package} directly, and the advisory applies to the installed version`, next: upgrade };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tewip-ai",
3
- "version": "0.18.3",
3
+ "version": "0.18.5",
4
4
  "description": "Tewip finds why each failing test failed - real bug, flaky test, environment, test data or changed selector - for any stack, from the command line, CI, or your editor.",
5
5
  "keywords": [
6
6
  "test-failures",