@produtype/core 0.46.0 → 0.48.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.
@@ -9,8 +9,28 @@ export interface ComplianceReference {
9
9
  export interface ComplianceObligation extends ComplianceReference {
10
10
  /** Findings that leave this obligation unmet. */
11
11
  findingIds: string[];
12
+ /**
13
+ * `unknown` where every check behind this obligation came back `unknown`.
14
+ *
15
+ * This was a boolean, and `met` was the default: an obligation whose only supporting
16
+ * check could not reach a verdict was reported as satisfied. Measured across
17
+ * seventy-eight repositories, 84 of 163 obligations marked met rested on not one
18
+ * passing check — most of them OWASP A01, broken access control, declared met
19
+ * because a single check said it did not know.
20
+ *
21
+ * The comment on the builder already stated the principle and the code applied it
22
+ * only halfway: it omitted obligations that nothing mapped to, and marked met the
23
+ * ones that mapped to silence.
24
+ */
25
+ status: 'met' | 'unmet' | 'unknown';
26
+ /** @deprecated Read `status`. True only where `status` is `met`. */
12
27
  met: boolean;
13
28
  }
29
+ /**
30
+ * Exported so a test can ask the same question the builder asks, rather than keeping
31
+ * a second copy of the prefix table that would drift from this one.
32
+ */
33
+ export declare function referencesFor(finding: Finding): ComplianceReference[];
14
34
  /**
15
35
  * Returns every obligation touched by the analysis, marking as unmet those with at
16
36
  * least one actionable finding attached. An obligation nothing mapped to is omitted
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.referencesFor = referencesFor;
3
4
  exports.buildComplianceMapping = buildComplianceMapping;
4
5
  /**
5
6
  * Maps findings onto the obligations a reader has to answer to.
@@ -82,6 +83,10 @@ const RULES = [
82
83
  references: [{ framework: 'owasp-top-10', reference: 'A08', title: 'Software and data integrity failures' }],
83
84
  },
84
85
  ];
86
+ /**
87
+ * Exported so a test can ask the same question the builder asks, rather than keeping
88
+ * a second copy of the prefix table that would drift from this one.
89
+ */
85
90
  function referencesFor(finding) {
86
91
  for (const rule of RULES) {
87
92
  if (finding.id.startsWith(rule.prefix))
@@ -108,15 +113,39 @@ function buildComplianceMapping(findings) {
108
113
  obligations.set(key, {
109
114
  ...reference,
110
115
  findingIds: actionable ? [finding.id] : [],
111
- met: !actionable,
116
+ status: 'unknown',
117
+ met: false,
118
+ verified: finding.status === 'passed',
112
119
  });
113
120
  continue;
114
121
  }
115
- if (actionable) {
122
+ if (actionable)
116
123
  existing.findingIds.push(finding.id);
117
- existing.met = false;
118
- }
124
+ if (finding.status === 'passed')
125
+ existing.verified = true;
119
126
  }
120
127
  }
121
- return [...obligations.values()].sort((left, right) => left.framework.localeCompare(right.framework) || left.reference.localeCompare(right.reference));
128
+ /**
129
+ * An obligation is met when something was checked and found right.
130
+ *
131
+ * Three outcomes, because the reader needs to tell them apart before quoting any of
132
+ * this to an auditor: something is wrong here, something was verified here, and
133
+ * nothing here reached a verdict.
134
+ */
135
+ for (const obligation of obligations.values()) {
136
+ obligation.status = obligation.findingIds.length > 0
137
+ ? 'unmet'
138
+ : obligation.verified ? 'met' : 'unknown';
139
+ obligation.met = obligation.status === 'met';
140
+ }
141
+ return [...obligations.values()]
142
+ .map((obligation) => ({
143
+ framework: obligation.framework,
144
+ reference: obligation.reference,
145
+ title: obligation.title,
146
+ findingIds: obligation.findingIds,
147
+ status: obligation.status,
148
+ met: obligation.met,
149
+ }))
150
+ .sort((left, right) => left.framework.localeCompare(right.framework) || left.reference.localeCompare(right.reference));
122
151
  }
@@ -5,7 +5,8 @@ export interface ExecutiveSummary {
5
5
  /** One sentence a non-technical reader can act on. */
6
6
  verdict: string;
7
7
  /** Whether the product can plausibly be launched as the selected profile. */
8
- launchReady: boolean;
8
+ /** `null` where the report could not judge the project at all. */
9
+ launchReady: boolean | null;
9
10
  /** The blocking themes, in plain language, worst first. */
10
11
  topRisks: string[];
11
12
  /** What the repository already does well, so the report is not only negative. */
@@ -123,7 +123,15 @@ function buildVerdict(args) {
123
123
  verdict: reason
124
124
  ? `ProdKit could not judge this project. ${reason}`
125
125
  : 'ProdKit could not recognise this project well enough to judge it. Check that the path points at application source.',
126
- launchReady: false,
126
+ /**
127
+ * `null`, not `false`.
128
+ *
129
+ * The verdict directly above says the project could not be judged, and the flag
130
+ * beside it said "not launch ready" — which the interface rendered as an amber
131
+ * badge reading exactly that. One of the two is a verdict about the product and
132
+ * the other is the absence of one; they cannot both be shown.
133
+ */
134
+ launchReady: null,
127
135
  };
128
136
  }
129
137
  if (!args.profile) {
@@ -202,7 +210,15 @@ function buildExecutiveSummary(args) {
202
210
  verdict,
203
211
  launchReady,
204
212
  topRisks,
205
- strengths: buildStrengths(args.categoryScores, args.findings),
213
+ /**
214
+ * A report that refuses to judge does not hand out compliments.
215
+ *
216
+ * A Python transcription script whose verdict read "ProdKit could not judge this
217
+ * project" listed three strengths underneath it, each of them a single check in a
218
+ * category nothing else had touched. The third place the same refusal sat next to
219
+ * the same praise.
220
+ */
221
+ strengths: args.inconclusive ? [] : buildStrengths(args.categoryScores, args.findings),
206
222
  estimatedEffort: estimateEffort(args.findings, args.profile),
207
223
  scoreExplanation,
208
224
  };
@@ -109,7 +109,7 @@ function executiveSummarySection(report) {
109
109
  '',
110
110
  summary.verdict,
111
111
  '',
112
- `- Launch ready: ${summary.launchReady ? 'yes' : 'no'}`,
112
+ `- Launch ready: ${summary.launchReady === null ? 'not judged' : summary.launchReady ? 'yes' : 'no'}`,
113
113
  `- Score: ${summary.scoreExplanation}`,
114
114
  `- Estimated effort: ${summary.estimatedEffort}`,
115
115
  '',
@@ -143,18 +143,40 @@ function categoryScoreSection(report) {
143
143
  function complianceSection(report) {
144
144
  if (report.compliance.length === 0)
145
145
  return [];
146
- const unmet = report.compliance.filter((obligation) => !obligation.met);
147
- if (unmet.length === 0)
146
+ /**
147
+ * Exposure and silence, listed apart.
148
+ *
149
+ * This filtered on `!met`, which was two different things wearing one label: an
150
+ * obligation with findings against it, and an obligation whose every check came
151
+ * back unknown. The second was printed under "Compliance Exposure" with a findings
152
+ * count of zero — a reader would quote that to an auditor as a gap when it is the
153
+ * absence of an answer.
154
+ */
155
+ const unmet = report.compliance.filter((obligation) => obligation.status === 'unmet');
156
+ const unassessed = report.compliance.filter((obligation) => obligation.status === 'unknown');
157
+ if (unmet.length === 0 && unassessed.length === 0)
148
158
  return [];
149
159
  return [
150
160
  '## Compliance Exposure',
151
161
  '',
152
162
  '_Advisory mapping, not a compliance certification. Obligations nothing mapped to are omitted rather than reported as met._',
153
163
  '',
154
- '| Framework | Reference | Obligation | Findings |',
155
- '| --- | --- | --- | --- |',
156
- ...unmet.map((obligation) => `| ${obligation.framework} | ${obligation.reference} | ${obligation.title} | ${obligation.findingIds.length} |`),
157
- '',
164
+ ...(unmet.length > 0
165
+ ? [
166
+ '| Framework | Reference | Obligation | Findings |',
167
+ '| --- | --- | --- | --- |',
168
+ ...unmet.map((obligation) => `| ${obligation.framework} | ${obligation.reference} | ${obligation.title} | ${obligation.findingIds.length} |`),
169
+ '',
170
+ ]
171
+ : ['No obligation in this mapping has a finding against it.', '']),
172
+ ...(unassessed.length > 0
173
+ ? [
174
+ `Not assessed — every check behind these came back unknown, so this report says nothing either way about ${unassessed.length === 1 ? 'it' : 'them'}:`,
175
+ '',
176
+ ...unassessed.map((obligation) => `- ${obligation.framework} ${obligation.reference}: ${obligation.title}`),
177
+ '',
178
+ ]
179
+ : []),
158
180
  ];
159
181
  }
160
182
  function renderMarkdown(report) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@produtype/core",
3
- "version": "0.46.0",
3
+ "version": "0.48.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": {