retire 6.0.0 → 6.1.0

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/CHANGELOG.md CHANGED
@@ -1,11 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## [6.1.0]
4
+
5
+ ### Improvements
6
+
7
+ - Repository validation errors (for an invalid `--jsrepo`, and from `repository/validate`) are now listed one per line with the location of each problem, instead of as a nested JSON tree:
8
+ ```
9
+ ✖ Invalid option: expected one of "none"|"low"|"medium"|"high"|"critical"
10
+ → at jquery.vulnerabilities[0].severity
11
+ ```
12
+ - Positional arguments (e.g. `retire .` or `retire src/`) are still ignored, but now print a warning. Use `--path <path>` to choose what to scan.
13
+ - Upgraded `commander` to 14 and `zod` to 4.
14
+
15
+ ### Bugfixes
16
+
17
+ - Repository validation crashed with a `TypeError` on extractor regexes that contain `§§version§§` without a capture group (e.g. `jquery-§§version§§\.js`), instead of reporting the invalid regex. It now reports `Regex must contain (§§version§§) as first capture group`.
18
+
3
19
  ## [6.0.0]
4
20
 
5
21
  ### Breaking changes
6
22
 
7
23
  - Requires Node.js 20.19+ or 22.12+ (previously 18+).
8
- - `--proxy` (and the `http_proxy` environment variable) now only supports `http://` and `https://` proxy URLs. SOCKS (`socks://`, `socks5://`, ...) and PAC (`pac+http://`, ...) proxies are no longer supported and fail with an `Unsupported proxy protocol` error.
24
+ - `--proxy` (and the `http_proxy` environment variable) now only supports `http://` and `https://` proxy URLs. SOCKS (`socks://`, `socks5://`, ...) and PAC (`pac+http://`, ...) proxies are no longer supported and fail with an `Unsupported proxy protocol` error (let us know if socks or pac are needed and we can re-add).
9
25
 
10
26
  ### Improvements
11
27
 
package/lib/cli.js CHANGED
@@ -79,6 +79,8 @@ const prg = commander_1.program
79
79
  .option('--cacert <path>', 'Use the specified certificate file to verify the peer used for fetching remote jsrepo/noderepo files')
80
80
  .option('--includeOsv', 'Include OSV advisories in the output')
81
81
  .option('--deep', 'Deep scan (slower and experimental)')
82
+ // Positional arguments were never used, but were silently accepted before commander 13.
83
+ .allowExcessArguments()
82
84
  .parse()
83
85
  .opts();
84
86
  const red = (x) => `\u001b[31m${x}\u001b[39m`;
@@ -180,6 +182,9 @@ try {
180
182
  throw new Error(`Invalid severity level (${severity}). Valid levels are: ${Object.keys(types_1.severityLevels).join(', ')}`);
181
183
  }
182
184
  log.info(`retire.js v${retire.version}`);
185
+ if (commander_1.program.args.length > 0) {
186
+ log.warn(colorwarn(`Warning: Ignoring unexpected argument(s) ${commander_1.program.args.map((arg) => `"${arg}"`).join(' ')}. Use --path <path> to choose what to scan (scanning ${scanpath}).`));
187
+ }
183
188
  if (prg.cacert) {
184
189
  if (!fs_1.default.existsSync(prg.cacert)) {
185
190
  throw new Error(`Could not read cacert file: ${prg.cacert}`);
package/lib/repo.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Options, Repository } from './types';
2
2
  import * as z from 'zod';
3
- export declare function validateRepository(repo: Repository, replacer?: Options['process']): z.SafeParseReturnType<unknown, Repository>;
3
+ export declare function validateRepository(repo: Repository, replacer?: Options['process']): z.ZodSafeParseResult<Repository>;
4
+ export declare function formatValidationError(error: z.ZodError): string;
4
5
  export declare function asbowerrepo(jsRepo: Repository): Repository;
5
6
  export declare function loadrepository(repoUrl: string, options: Options): Promise<Repository>;
6
7
  export declare function loadrepositoryFromFile(filepath: string, options: Options): Promise<Repository>;
package/lib/repo.js CHANGED
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.validateRepository = validateRepository;
37
+ exports.formatValidationError = formatValidationError;
37
38
  exports.asbowerrepo = asbowerrepo;
38
39
  exports.loadrepository = loadrepository;
39
40
  exports.loadrepositoryFromFile = loadrepositoryFromFile;
@@ -88,47 +89,45 @@ function validateRepository(repo, replacer) {
88
89
  .strict()
89
90
  .superRefine((o, ctx) => {
90
91
  if (Object.keys(o).filter((k) => k != 'summary').length == 0)
91
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Must have at least one identifier' });
92
+ ctx.addIssue({ code: 'custom', message: 'Must have at least one identifier' });
92
93
  const ids = Object.values(o)
93
94
  .map((x) => (Array.isArray(x) ? x : [x]))
94
95
  .reduce((a, b) => a.concat(b), []).length;
95
96
  if (ids == 0)
96
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Must have at least one identifier' });
97
+ ctx.addIssue({ code: 'custom', message: 'Must have at least one identifier' });
97
98
  }),
98
99
  details: z.string().optional(),
99
100
  info: z.array(z.string().regex(/^https?:\/\/.+/)),
100
101
  })
101
102
  .strict();
102
103
  const regexValidator = z.string().superRefine((s, ctx) => {
103
- if (ctx.path[0] == 'dont check')
104
- return;
105
104
  try {
106
105
  new RegExp(s);
107
106
  }
108
107
  catch {
109
108
  ctx.addIssue({
110
- code: z.ZodIssueCode.custom,
109
+ code: 'custom',
111
110
  message: 'Invalid regex: ' + s,
112
111
  });
113
112
  }
114
113
  if (s.includes('[]'))
115
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Regex must not contain []: ' + s });
114
+ ctx.addIssue({ code: 'custom', message: 'Regex must not contain []: ' + s });
116
115
  if (s.includes('{}'))
117
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Regex must not contain {}: ' + s });
116
+ ctx.addIssue({ code: 'custom', message: 'Regex must not contain {}: ' + s });
118
117
  [/.*[^\\]\{[^0-9,\\]\}.*/, /[^,0-9\\]\}/, /[^\\]\{[^,0-9]/].forEach((r) => {
119
118
  if (r.test(s))
120
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'There is something odd with this regex: ' + s });
119
+ ctx.addIssue({ code: 'custom', message: 'There is something odd with this regex: ' + s });
121
120
  });
122
121
  let versionMatcher = '§§version§§';
123
122
  if (replacer)
124
123
  versionMatcher = JSON.parse(`"${replacer(versionMatcher)}"`);
125
124
  const versionIndex = s.indexOf(versionMatcher);
126
125
  if (versionIndex == -1 || (versionIndex > 0 && s.substring(versionIndex - 1, versionIndex) == '\\')) {
127
- ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Regex must contain (§§version§§): ' + s });
126
+ ctx.addIssue({ code: 'custom', message: 'Regex must contain (§§version§§): ' + s });
128
127
  }
129
- else if (s.replace(/\(\?:/g, '').replace(/\\\(/g, '').split(/\(/)[1].indexOf(versionMatcher) != 0) {
128
+ else if (s.replace(/\(\?:/g, '').replace(/\\\(/g, '').split(/\(/)[1]?.indexOf(versionMatcher) != 0) {
130
129
  ctx.addIssue({
131
- code: z.ZodIssueCode.custom,
130
+ code: 'custom',
132
131
  message: 'Regex must contain (§§version§§) as first capture group: ' + s.replace(/\(\?:/g, '').replace(/\\\(/g, ''),
133
132
  });
134
133
  }
@@ -136,7 +135,7 @@ function validateRepository(repo, replacer) {
136
135
  const replaceValidator = z
137
136
  .string()
138
137
  .regex(/^\/(.*[^\\])\/([^/]+)\/$/, 'RegExp error - should be on format "/search/replacement/"');
139
- const validator = z.record(z
138
+ const entryValidator = (extractorValidator) => z
140
139
  .object({
141
140
  bowername: z.array(z.string().regex(/^[a-z0-9.-]+$/i)).optional(),
142
141
  basePurl: z
@@ -151,9 +150,9 @@ function validateRepository(repo, replacer) {
151
150
  extractors: z
152
151
  .object({
153
152
  func: z.array(z.string().min(5)).optional(),
154
- uri: z.array(regexValidator).optional(),
155
- filename: z.array(regexValidator).optional(),
156
- filecontent: z.array(regexValidator).optional(),
153
+ uri: z.array(extractorValidator).optional(),
154
+ filename: z.array(extractorValidator).optional(),
155
+ filecontent: z.array(extractorValidator).optional(),
157
156
  filecontentreplace: z.array(replaceValidator).optional(),
158
157
  hashes: z.record(z.string().regex(/^[a-f0-9]+$/i), versionValidator).optional(),
159
158
  ast: z.array(z.string()).optional(),
@@ -165,11 +164,15 @@ function validateRepository(repo, replacer) {
165
164
  .regex(/(\([A-Za-z\-0-9.]+( OR [A-Za-z\-0-9.]+)+\)|[A-Za-z\-0-9.]+) >=[0-9.]+( <[0-9.]+)?(;>=[0-9.]+( <[0-9.]+)?)*/))
166
165
  .optional(),
167
166
  })
168
- .strict());
167
+ .strict();
168
+ // "dont check" lists URIs that are deliberately not version extractors, so its regexes are not validated.
169
+ const validator = z
170
+ .object({ 'dont check': entryValidator(z.string()).optional() })
171
+ .catchall(entryValidator(regexValidator));
169
172
  return validator.safeParse(repo);
170
173
  }
171
174
  function formatValidationError(error) {
172
- return JSON.stringify(error.format(), (key, value) => (Array.isArray(value) && value.length === 0 ? undefined : value), 2);
175
+ return z.prettifyError(error);
173
176
  }
174
177
  async function loadJson(url, options) {
175
178
  options.log.info('Downloading ' + url + ' ...');
@@ -198,7 +201,7 @@ async function loadJson(url, options) {
198
201
  resolve(json);
199
202
  }
200
203
  else {
201
- reject(`Invalid repository from ${url}: ${formatValidationError(vresult.error)}`);
204
+ reject(`Invalid repository from ${url}:\n${formatValidationError(vresult.error)}`);
202
205
  }
203
206
  }
204
207
  catch (error) {
@@ -222,7 +225,7 @@ async function loadJsonFromFile(file, options) {
222
225
  resolve(json);
223
226
  }
224
227
  else {
225
- reject(`Invalid repository from ${file}: ${formatValidationError(vresult.error)}`);
228
+ reject(`Invalid repository from ${file}:\n${formatValidationError(vresult.error)}`);
226
229
  }
227
230
  }
228
231
  catch (error) {
package/lib/retire.js CHANGED
@@ -4,7 +4,7 @@
4
4
  */
5
5
 
6
6
  var exports = exports || {};
7
- exports.version = '6.0.0';
7
+ exports.version = '6.1.0';
8
8
 
9
9
  function isDefined(o) {
10
10
  return typeof o !== 'undefined';
package/lib/types.d.ts CHANGED
@@ -56,7 +56,13 @@ export declare const severityLevels: {
56
56
  readonly critical: 4;
57
57
  };
58
58
  export type SeverityLevel = keyof typeof severityLevels;
59
- export declare const severityParser: z.ZodEnum<["none" | "low" | "medium" | "high" | "critical"]>;
59
+ export declare const severityParser: z.ZodEnum<{
60
+ none: "none";
61
+ low: "low";
62
+ medium: "medium";
63
+ high: "high";
64
+ critical: "critical";
65
+ }>;
60
66
  export type PathDescriptor = {
61
67
  path: string;
62
68
  justification?: string;
@@ -97,27 +103,11 @@ export type Options = {
97
103
  };
98
104
  export declare const ignoreFileParser: z.ZodArray<z.ZodIntersection<z.ZodObject<{
99
105
  justification: z.ZodString;
100
- }, "strip", z.ZodTypeAny, {
101
- justification: string;
102
- }, {
103
- justification: string;
104
- }>, z.ZodUnion<[z.ZodObject<{
106
+ }, z.core.$strip>, z.ZodUnion<[z.ZodObject<{
105
107
  path: z.ZodString;
106
- }, "strip", z.ZodTypeAny, {
107
- path: string;
108
- }, {
109
- path: string;
110
- }>, z.ZodObject<{
108
+ }, z.core.$strip>, z.ZodObject<{
111
109
  component: z.ZodString;
112
110
  version: z.ZodOptional<z.ZodString>;
113
111
  identifiers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
114
- }, "strip", z.ZodTypeAny, {
115
- component: string;
116
- version?: string | undefined;
117
- identifiers?: Record<string, string> | undefined;
118
- }, {
119
- component: string;
120
- version?: string | undefined;
121
- identifiers?: Record<string, string> | undefined;
122
- }>]>>, "many">;
112
+ }, z.core.$strip>]>>>;
123
113
  export {};
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "author": "Erlend Oftedal <erlend@oftedal.no>",
3
3
  "name": "retire",
4
4
  "description": "Retire is a tool for detecting use of vulnerable libraries",
5
- "version": "6.0.0",
5
+ "version": "6.1.0",
6
6
  "license": "Apache-2.0",
7
7
  "repository": {
8
8
  "type": "git",
@@ -14,9 +14,9 @@
14
14
  "main": "./lib/retire.js",
15
15
  "dependencies": {
16
16
  "astronomical": "^3.2.0",
17
- "commander": "^10.0.1",
17
+ "commander": "^14.0.3",
18
18
  "walkdir": "0.4.1",
19
- "zod": "^3.22.4"
19
+ "zod": "^4.6.5"
20
20
  },
21
21
  "devDependencies": {
22
22
  "@eslint/js": "^10.0.1",