@patrickdk77/archive-defuse 0.1.3 → 0.2.1

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/README.md CHANGED
@@ -175,6 +175,8 @@ Every file entry goes to `filePlugins` in order, with pdf-defuse's shapes. A plu
175
175
 
176
176
  The plugin's findings come into the report with `attachment` set to the entry's name. A `reject` inside the entry becomes `strip`, since the entry goes and the archive stays. When no plugin accepts an entry, ENTRY/NO_PLUGIN removes it. As `info`, the entry stays.
177
177
 
178
+ A plugin's answer is final, and its findings are all the entry adds to the score. ENTRY/PLUGIN_PASSED and PLUGIN_SCRUBBED weigh 0, in the upload's score and the output's. ENTRY/PLUGIN_FAILED weighs 30, what NO_PLUGIN weighs, since no plugin answered for the entry.
179
+
178
180
  Before the plugins, a type check reports ENTRY/TYPE_MISMATCH (`info`) when the name's extension and the content disagree, as pdf-defuse does for attached files. A plugin never checks types.
179
181
 
180
182
  `passThrough(types)` keeps entries by MIME type or extension, such as `passThrough(['text/csv', '.txt'])`.
package/dist/engine.js CHANGED
@@ -38,7 +38,6 @@ async function endRun(run) {
38
38
  await run.temp.cleanup();
39
39
  }
40
40
  }
41
- const PLUGIN_KEPT = new Set([types_1.ArchiveDetail.PluginPassed, types_1.ArchiveDetail.PluginScrubbed]);
42
41
  /** Kinds whose content the rewrite drops whatever their action, so an info override leaves CONTENT_REMOVED. */
43
42
  const DROPPED_ANYWAY = new Set([types_1.ArchiveDetail.HiddenData, types_1.ArchiveDetail.NonFileData, types_1.ArchiveDetail.ExtraField]);
44
43
  /**
@@ -52,7 +51,9 @@ function finish(findings, options, incomplete, archive, isOutput = false) {
52
51
  const rejected = findings.some(f => f.action === 'reject');
53
52
  // In an upload, a plugin rewrite or a metadata strip changes the file, so the output must be written.
54
53
  const strippable = findings.some(f => f.action === 'strip' || (!isOutput && (f.detail === types_1.ArchiveDetail.PluginScrubbed || f.detail === types_1.ArchiveDetail.Stripped) && f.attachment === undefined));
55
- const { score, risk } = (0, score_1.scoreFindings)(isOutput ? findings : findings.filter(f => !PLUGIN_KEPT.has(f.detail)), options);
54
+ // What a plugin decided counts at the weights of the plugin's own findings; the PLUGIN_* findings that record its
55
+ // answer weigh nothing.
56
+ const { score, risk } = (0, score_1.scoreFindings)(findings, options);
56
57
  return { status: rejected ? 'rejected' : strippable ? 'strippable' : 'clean', score, risk, findings, ...named };
57
58
  }
58
59
  /** A value from the archive, such as a name, cut to a length a report can carry. */
package/dist/findings.js CHANGED
@@ -89,9 +89,9 @@ def(types_1.ArchiveCategory.Compression, types_1.ArchiveDetail.UnsupportedFeatur
89
89
  // ENTRY
90
90
  def(types_1.ArchiveCategory.Entry, types_1.ArchiveDetail.NoPlugin, 'strip', 30, '<Entry "{name}"|An entry> (<{sniffed}|unknown type>) removed. No plugin accepts this type');
91
91
  def(types_1.ArchiveCategory.Entry, types_1.ArchiveDetail.TypeMismatch, 'info', 40, 'The types of <entry "{name}"|an unnamed entry> disagree: <its name says {nameType}|its name has no known extension>, and <its content looks like {sniffed}|its content is not recognized>');
92
- def(types_1.ArchiveCategory.Entry, types_1.ArchiveDetail.PluginPassed, 'info', 20, 'Entry kept by plugin <plugin> with nothing removed');
93
- def(types_1.ArchiveCategory.Entry, types_1.ArchiveDetail.PluginScrubbed, 'info', 10, 'Entry cleaned and kept by plugin <plugin>');
94
- def(types_1.ArchiveCategory.Entry, types_1.ArchiveDetail.PluginFailed, 'strip', 0, 'Plugin <plugin> failed, so the entry was removed');
92
+ def(types_1.ArchiveCategory.Entry, types_1.ArchiveDetail.PluginPassed, 'info', 0, 'Entry kept by plugin <plugin> with nothing removed');
93
+ def(types_1.ArchiveCategory.Entry, types_1.ArchiveDetail.PluginScrubbed, 'info', 0, 'Entry cleaned and kept by plugin <plugin>');
94
+ def(types_1.ArchiveCategory.Entry, types_1.ArchiveDetail.PluginFailed, 'strip', 30, 'Plugin <plugin> failed, so the entry was removed');
95
95
  def(types_1.ArchiveCategory.Entry, types_1.ArchiveDetail.PluginRemoved, 'strip', 0, 'Entry removed by plugin <plugin>');
96
96
  // METADATA
97
97
  def(types_1.ArchiveCategory.Metadata, types_1.ArchiveDetail.Comment, 'info', 0, 'The archive or an entry carries a comment');
package/dist/tar.js CHANGED
@@ -133,7 +133,7 @@ class TarArchive {
133
133
  const reader = new io_1.Reader(source, size);
134
134
  const notes = [];
135
135
  const entries = [];
136
- let global = [];
136
+ let globalRecords = [];
137
137
  let pending = { pax: [] };
138
138
  let pos = 0;
139
139
  let total = 0;
@@ -203,7 +203,7 @@ class TarArchive {
203
203
  if (typeflag === 'g') {
204
204
  // Later global records replace earlier ones of the same key.
205
205
  const keys = new Set(parsed.records.map(r => r.key));
206
- global = [...global.filter(r => !keys.has(r.key)), ...parsed.records];
206
+ globalRecords = [...globalRecords.filter(r => !keys.has(r.key)), ...parsed.records];
207
207
  const c = parsed.records.find(r => r.key === 'comment');
208
208
  if (c)
209
209
  comment = c.value.toString('utf8');
@@ -230,7 +230,7 @@ class TarArchive {
230
230
  continue;
231
231
  }
232
232
  // The records that apply: global ones, then this entry's own, the later winning.
233
- const records = [...global.filter(g => !pending.pax.some(r => r.key === g.key)), ...pending.pax];
233
+ const records = [...globalRecords.filter(g => !pending.pax.some(r => r.key === g.key)), ...pending.pax];
234
234
  const last = (key) => {
235
235
  let found;
236
236
  for (const r of records)
@@ -315,7 +315,7 @@ class TarArchive {
315
315
  devmajor: posix || ustar === 'ustar ' ? num(329, 8, 'devmajor') : 0,
316
316
  devminor: posix || ustar === 'ustar ' ? num(337, 8, 'devminor') : 0,
317
317
  // Global records apply to every entry after them, so each entry keeps its own copy. A global comment is the archive's.
318
- pax: records.filter(r => !isStructural(r.key) && !(r.key === 'comment' && global.includes(r))),
318
+ pax: records.filter(r => !isStructural(r.key) && !(r.key === 'comment' && globalRecords.includes(r))),
319
319
  };
320
320
  if (kind === 'symlink' || kind === 'hardlink')
321
321
  e.linkTarget = linkTarget;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patrickdk77/archive-defuse",
3
- "version": "0.1.3",
3
+ "version": "0.2.1",
4
4
  "description": "Sanitize zip, tar, gzip, bzip2, zstd, Brotli, cab, 7z, xz and lzma archives. Detects path traversal, links out of the tree, device files, duplicate names, overlapping entries, hidden and prepended data, encrypted entries and compression bombs, hands every entry to file plugins, scores the risk, and writes a defused archive in the same format with no runtime dependencies.",
5
5
  "license": "MIT AND Apache-2.0",
6
6
  "author": "patrickdk77",