@produtype/core 0.22.0 → 0.23.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.
@@ -30,12 +30,22 @@ async function detectUploads(ctx) {
30
30
  const uploadDeps = (0, detectContext_1.hasAnyDep)(ctx, ['multer', 'formidable', '@aws-sdk/client-s3', 'aws-sdk']);
31
31
  const pyUploadDeps = (0, detectContext_1.hasAnyPyDep)(ctx, ['boto3']);
32
32
  const avDeps = (0, detectContext_1.hasAnyDep)(ctx, ['clamav', 'clamscan']);
33
+ /**
34
+ * Each line tagged with the claim it supports.
35
+ *
36
+ * Everything this detector finds used to land in one array, so a report that said "no
37
+ * upload handling was found in this repository" cited six snippets under it — five of
38
+ * them `LoginRequiredMixin` and `@login_required`, one a PDF content type — and was
39
+ * stamped "confidence: high, evidence quality: strong" for them. That a project has
40
+ * authentication is why an upload check would matter; it is not evidence about
41
+ * uploads, and the rate-limit rule already carries that sentence for the same reason.
42
+ */
33
43
  for (const d of uploadDeps)
34
- evidence.push({ type: 'dependency', value: d });
44
+ evidence.push({ type: 'dependency', value: d, claim: 'uploads' });
35
45
  for (const d of pyUploadDeps)
36
- evidence.push({ type: 'dependency', value: d });
46
+ evidence.push({ type: 'dependency', value: d, claim: 'uploads' });
37
47
  for (const d of avDeps)
38
- evidence.push({ type: 'dependency', value: d });
48
+ evidence.push({ type: 'dependency', value: d, claim: 'antivirus' });
39
49
  const routeSignals = [];
40
50
  for (const file of ctx.files.source) {
41
51
  const text = await (0, readTextFileSafe_1.readTextFileSafe)(ctx.root, file);
@@ -74,18 +84,18 @@ async function detectUploads(ctx) {
74
84
  /@permission_required/,
75
85
  ], 15);
76
86
  for (const m of djangoProtectionSignals) {
77
- evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line });
87
+ evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'protection' });
78
88
  }
79
89
  const validationSignals = await (0, textSearch_1.searchInFiles)(ctx.root, source, [/file-type/i, /mime/i, /content-type/i], 15);
80
90
  const protectedRoutes = routeSignals.filter((r) => r.protected);
81
91
  const unprotectedRoutes = routeSignals.filter((r) => !r.protected);
82
92
  for (const r of routeSignals) {
83
- evidence.push({ type: 'snippet', value: r.snippet, file: r.file, line: r.line });
93
+ evidence.push({ type: 'snippet', value: r.snippet, file: r.file, line: r.line, claim: 'uploads' });
84
94
  }
85
95
  for (const m of djangoPublicSignals)
86
- evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line });
96
+ evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'uploads' });
87
97
  for (const m of validationSignals)
88
- evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line });
98
+ evidence.push({ type: 'snippet', value: m.snippet, file: m.file, line: m.line, claim: 'validation' });
89
99
  const publicExposure = unprotectedRoutes.length > 0 || djangoPublicSignals.length > 0;
90
100
  const protectedSomehow = protectedRoutes.length > 0 || djangoProtectionSignals.length > 0;
91
101
  return {
@@ -86,7 +86,22 @@ function buildCategoryScores(findings) {
86
86
  * critical and nine passing checks is not 90% healthy". The cap keeps that;
87
87
  * the proportion does the ranking underneath it.
88
88
  */
89
- const worstOpen = actionable.reduce((worst, finding) => (SEVERITY_WEIGHT[finding.stakes] > SEVERITY_WEIGHT[worst] ? finding.stakes : worst), 'info');
89
+ /**
90
+ * The ceiling follows what the report says is wrong, not what the area is worth.
91
+ *
92
+ * These are two different numbers on purpose: `stakes` is what a capability is worth
93
+ * before confidence is folded in, and `severity` is what the report is willing to
94
+ * claim about it. Using stakes here capped a category at the critical ceiling while
95
+ * the row beside it read "0 critical" — a Django project with four of five security
96
+ * checks verified and one high finding open scored 20 of 100, below an area with one
97
+ * of three verified.
98
+ *
99
+ * The proportion below still uses stakes, which is right: how much of what is at
100
+ * stake is failing does not change because the report hedged. What changes is the
101
+ * cap, and a cap that says "nothing looks healthy while something severe is open"
102
+ * has to mean the severity the reader was shown.
103
+ */
104
+ const worstOpen = actionable.reduce((worst, finding) => (SEVERITY_WEIGHT[finding.severity] > SEVERITY_WEIGHT[worst] ? finding.severity : worst), 'info');
90
105
  const ceiling = SEVERITY_CEILING[worstOpen];
91
106
  return {
92
107
  category,
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.rules = void 0;
4
4
  const inferProductProfile_1 = require("../expectations/inferProductProfile");
5
+ const absenceEvidence_1 = require("../analyzer/absenceEvidence");
5
6
  function statusFromFlags(present, complete) {
6
7
  if (!present)
7
8
  return 'missing';
@@ -484,7 +485,20 @@ exports.rules = [
484
485
  ? 'Upload handling was found, and none of it is publicly exposed.'
485
486
  : 'No upload handling was found in this repository.',
486
487
  recommendation: 'Protect upload routes with authz, validate MIME/type, and prefer private object storage.',
487
- evidence: up?.evidence ?? [],
488
+ /**
489
+ * The evidence follows the claim.
490
+ *
491
+ * "No upload handling was found in this repository" used to arrive with six
492
+ * snippets under it, five of them Django auth decorators, and a confidence of
493
+ * high earned from them. Where nothing was found, the honest citation is the
494
+ * terms that were looked for.
495
+ */
496
+ evidence: status === 'unknown'
497
+ ? (0, absenceEvidence_1.searchedFor)('upload handling', ['multer', 'formidable', 'django-storages', 'FileField', '/uploads', 'express.static'])
498
+ // The routes and the public-serving lines, which is what this claim is about.
499
+ // Validation and antivirus signals are tagged separately and belong to other
500
+ // questions.
501
+ : evidenceForClaim(up?.evidence, 'uploads'),
488
502
  });
489
503
  },
490
504
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.22.0",
3
+ "version": "0.23.0",
4
4
  "description": "Deterministic CLI and library that analyzes a web application repository and reports how far it is from production-ready for the kind of product it is meant to be.",
5
5
  "license": "MIT",
6
6
  "bin": {