@produtype/core 0.22.1 → 0.24.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 {
@@ -1,3 +1,16 @@
1
1
  import type { Finding } from './types';
2
+ /**
3
+ * There is no fallback, and that is the point.
4
+ *
5
+ * There was one, and it said things like "a common attack is not defended against" and
6
+ * "a privacy obligation is unmet" — the finding's own title with the detail removed.
7
+ * A hundred and two actionable findings in the verification corpus carried one of those
8
+ * twelve sentences, under a heading that promises what actually happens.
9
+ *
10
+ * Saying nothing is better: the finding, its evidence and its recommendation are still
11
+ * there, and a reader is not handed a sentence that adds a line and no information. A
12
+ * test keeps the list honest by failing when a finding the fixtures produce has no
13
+ * specific sentence.
14
+ */
2
15
  export declare function businessImpactFor(finding: Finding): string | undefined;
3
16
  export declare function withBusinessImpact(findings: Finding[]): Finding[];
@@ -48,26 +48,56 @@ const IMPACT_BY_FINDING = [
48
48
  ['security.rate-limit', 'Credentials can be guessed in bulk and expensive endpoints can be used to run up your bill.'],
49
49
  ['uploads.public-exposure', 'Uploaded files are reachable by anyone who knows or guesses the path.'],
50
50
  ['billing.webhook', 'Payment notifications are not verified, so billing state can be changed by a forged request.'],
51
+ /**
52
+ * The profiles that are not a web application.
53
+ *
54
+ * This table was written for server products and never grew when `client-app`,
55
+ * `mobile-app` and `library` arrived, so a hundred and twenty-six actionable findings
56
+ * across the verification corpus said nothing at all here — every one of them in a
57
+ * product that is not a web application, which is the case this tool exists to judge
58
+ * on its own terms.
59
+ */
60
+ ['expectation.app.state-durability', 'Work the person has done lives only in storage the platform may clear, so a cleared cache, a private window or a reinstall loses it silently.'],
61
+ ['expectation.app.asset-delivery', 'The first screen waits on the network every time, and on a bad connection the product looks broken rather than slow.'],
62
+ ['expectation.client.error-reporting', 'A crash on somebody else device is invisible: the screen goes white, the person leaves, and nothing reaches anybody who could fix it.'],
63
+ ['expectation.mobile.offline', 'A network that comes and goes is the normal state of a phone, so the product spends part of every day showing a spinner in a lift or on a train.'],
64
+ ['expectation.mobile.credential-storage', 'A token in preferences or a plain file is readable on a rooted device and often travels into a backup, which is where it leaks from.'],
65
+ ['expectation.mobile.permissions', 'A permission asked with no reason attached is refused more often, and on iOS a missing purpose string is a review rejection rather than a warning.'],
66
+ ['expectation.mobile.forced-update', 'A version installed months ago keeps talking to the server whatever the release notes say, so a broken client cannot be retired.'],
67
+ ['expectation.mobile.privacy-declaration', 'What the store listing promises about data lives in a dashboard rather than beside the code, so it stops matching what the app collects and nobody notices.'],
68
+ ['expectation.packaging.license', 'Without a licence nobody may legally use the package, and a company that checks will refuse it at the dependency review.'],
69
+ ['expectation.packaging.entrypoints', 'Whoever installs the package cannot import it: what is published has no stated way in.'],
70
+ ['expectation.packaging.metadata', 'The registry cannot say what this is or who maintains it, so the package is hard to find and harder to trust.'],
71
+ ['expectation.docs.readme', 'Somebody who finds the package has nothing telling them what it does, so they move on.'],
72
+ ['expectation.quality.tests', 'Nothing proves the package works, so every release is a guess and every regression is found by a user.'],
73
+ ['expectation.quality.ci', 'The tests run when somebody remembers, which over a few months means they stop running.'],
74
+ ['expectation.b2c.notifications', 'There is no way to reach somebody after they close the tab: no confirmation, no password reset, no notice that something happened to their account.'],
75
+ ['expectation.b2c.onboarding', 'A new account lands on an empty product with nothing to do first, which is where most of them stop.'],
76
+ ['expectation.auth.api-keys', 'Machine access has to borrow a human session, so a script that stops working is indistinguishable from somebody signing out — and a leaked key cannot be revoked on its own.'],
77
+ ['auth.core', 'Anyone who finds a URL can use the product and read whatever it exposes. There is no notion of "your" data.'],
78
+ ['authz.resource-level', 'A normal user can read or change another user records by changing an identifier in the URL.'],
79
+ ['docker.presence', 'Environments drift between local, CI and production, and "works on my machine" becomes unfalsifiable.'],
80
+ ['env.example', 'Somebody setting the project up has to read the source to find out which variables it needs, and a missing one fails at runtime rather than at startup.'],
81
+ ['security.django-debug', 'With DEBUG on, an error page shows the stack, the settings and often the database contents to whoever triggered it.'],
82
+ ['security.django-secure-cookies', 'Session and CSRF cookies travel in the clear, so anybody on the same network can take a signed-in session.'],
51
83
  ['tenancy', 'Customer data is not separated, so one customer can reach another data.'],
52
84
  ['gdpr', 'A privacy obligation is unmet, and the obligation applies whether or not the feature exists.'],
53
85
  ['observability', 'Production behaviour is not visible, so problems are found late and diagnosed slowly.'],
54
86
  ['deployment', 'Releases are not reproducible, which makes recovery depend on individual memory.'],
55
87
  ];
56
- /** Fallback when no specific mapping matches, so every actionable finding says something. */
57
- const IMPACT_BY_CATEGORY = {
58
- auth: 'Access to the product is not properly controlled.',
59
- authz: 'Users can act beyond what their role should allow.',
60
- tenancy: 'Customer data is not reliably separated.',
61
- gdpr: 'A privacy obligation is unmet.',
62
- security: 'A common attack is not defended against.',
63
- uploads: 'Uploaded files are not properly controlled.',
64
- billing: 'Revenue handling is incomplete or unverified.',
65
- audit: 'Sensitive actions cannot be attributed after the fact.',
66
- observability: 'Production behaviour is not visible.',
67
- jobs: 'Long-running work is not handled reliably.',
68
- deployment: 'Releases are not reproducible.',
69
- env: 'Configuration or secrets are handled unsafely.',
70
- };
88
+ /**
89
+ * There is no fallback, and that is the point.
90
+ *
91
+ * There was one, and it said things like "a common attack is not defended against" and
92
+ * "a privacy obligation is unmet" — the finding's own title with the detail removed.
93
+ * A hundred and two actionable findings in the verification corpus carried one of those
94
+ * twelve sentences, under a heading that promises what actually happens.
95
+ *
96
+ * Saying nothing is better: the finding, its evidence and its recommendation are still
97
+ * there, and a reader is not handed a sentence that adds a line and no information. A
98
+ * test keeps the list honest by failing when a finding the fixtures produce has no
99
+ * specific sentence.
100
+ */
71
101
  function businessImpactFor(finding) {
72
102
  if (finding.status === 'passed')
73
103
  return undefined;
@@ -75,7 +105,7 @@ function businessImpactFor(finding) {
75
105
  if (finding.id.startsWith(prefix))
76
106
  return impact;
77
107
  }
78
- return IMPACT_BY_CATEGORY[finding.category];
108
+ return undefined;
79
109
  }
80
110
  /** Attaches the business impact to every actionable finding. */
81
111
  /**
@@ -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.1",
3
+ "version": "0.24.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": {