@blamejs/core 0.8.9 → 0.8.11
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 +4 -0
- package/index.js +8 -1
- package/lib/ai-adverse-decision.js +184 -0
- package/lib/breach-deadline.js +272 -0
- package/lib/compliance-eaa.js +204 -0
- package/lib/cra-report.js +195 -0
- package/lib/gdpr-ropa.js +261 -0
- package/lib/middleware/age-gate.js +141 -0
- package/lib/middleware/bot-disclose.js +149 -0
- package/lib/middleware/index.js +6 -0
- package/lib/nis2-report.js +181 -0
- package/package.json +1 -1
- package/sbom.cyclonedx.json +6 -6
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* ageGate middleware — request-level age classification + high-privacy
|
|
4
|
+
* default headers for routes that need stricter handling for users
|
|
5
|
+
* below an operator-configured age threshold.
|
|
6
|
+
*
|
|
7
|
+
* COPPA (US, 13 and under), UK Children's Code (16 and under),
|
|
8
|
+
* California AADC (18 and under), and similar regimes require
|
|
9
|
+
* operators to apply heightened privacy protections when serving
|
|
10
|
+
* users below a regulatory age. The middleware:
|
|
11
|
+
*
|
|
12
|
+
* 1. Reads the operator's `getAge(req)` predicate to classify the
|
|
13
|
+
* request as "above-threshold" / "below-threshold" / "unknown"
|
|
14
|
+
* 2. Sets high-privacy defaults on below-threshold + unknown
|
|
15
|
+
* responses:
|
|
16
|
+
* - `Cache-Control: private, no-store`
|
|
17
|
+
* - `Referrer-Policy: no-referrer`
|
|
18
|
+
* - `X-Privacy-Posture: below-threshold`
|
|
19
|
+
* 3. Refuses with 451 (Unavailable For Legal Reasons) when the
|
|
20
|
+
* operator-supplied requireAge: 18 is set and the request is
|
|
21
|
+
* below threshold without a parental-consent record
|
|
22
|
+
* 4. Audits the classification decision
|
|
23
|
+
*
|
|
24
|
+
* var gate = b.middleware.ageGate({
|
|
25
|
+
* getAge: function (req) {
|
|
26
|
+
* if (req.user && typeof req.user.age === "number") return req.user.age;
|
|
27
|
+
* return null; // unknown
|
|
28
|
+
* },
|
|
29
|
+
* requireAge: null, // null = don't gate, just headers
|
|
30
|
+
* consentRequired: 18, // require parental consent below this
|
|
31
|
+
* hasParentalConsent: function (req) {
|
|
32
|
+
* return req.user && req.user.parentalConsent === true;
|
|
33
|
+
* },
|
|
34
|
+
* });
|
|
35
|
+
* router.use(gate);
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
var defineClass = require("../framework-error").defineClass;
|
|
39
|
+
var lazyRequire = require("../lazy-require");
|
|
40
|
+
var validateOpts = require("../validate-opts");
|
|
41
|
+
|
|
42
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
43
|
+
|
|
44
|
+
var AgeGateError = defineClass("AgeGateError", { alwaysPermanent: true });
|
|
45
|
+
|
|
46
|
+
function create(opts) {
|
|
47
|
+
opts = opts || {};
|
|
48
|
+
validateOpts(opts, [
|
|
49
|
+
"audit", "getAge", "requireAge", "consentRequired",
|
|
50
|
+
"hasParentalConsent", "skipPaths", "errorMessage",
|
|
51
|
+
], "middleware.ageGate");
|
|
52
|
+
|
|
53
|
+
if (typeof opts.getAge !== "function") {
|
|
54
|
+
throw new AgeGateError("age-gate/bad-get-age",
|
|
55
|
+
"middleware.ageGate: opts.getAge must be a function (req) -> number | null");
|
|
56
|
+
}
|
|
57
|
+
var getAge = opts.getAge;
|
|
58
|
+
var requireAge = (typeof opts.requireAge === "number" && opts.requireAge > 0) // allow:numeric-opt-Infinity — age is operator domain, not a bytes/time-shaped opt
|
|
59
|
+
? opts.requireAge : null;
|
|
60
|
+
var consentRequired = (typeof opts.consentRequired === "number" && opts.consentRequired > 0) // allow:numeric-opt-Infinity — age threshold, not a bytes/time-shaped opt
|
|
61
|
+
? opts.consentRequired : null;
|
|
62
|
+
var hasParentalConsent = typeof opts.hasParentalConsent === "function" ? opts.hasParentalConsent : null;
|
|
63
|
+
var skipPaths = Array.isArray(opts.skipPaths) ? opts.skipPaths.slice() : [];
|
|
64
|
+
var auditOn = opts.audit !== false;
|
|
65
|
+
var errorMessage = typeof opts.errorMessage === "string" && opts.errorMessage.length > 0
|
|
66
|
+
? opts.errorMessage : "service unavailable without parental consent";
|
|
67
|
+
|
|
68
|
+
function _shouldSkip(req) {
|
|
69
|
+
if (skipPaths.length === 0) return false;
|
|
70
|
+
var p = req.url || "";
|
|
71
|
+
var qpos = p.indexOf("?");
|
|
72
|
+
if (qpos !== -1) p = p.slice(0, qpos);
|
|
73
|
+
for (var i = 0; i < skipPaths.length; i++) {
|
|
74
|
+
var s = skipPaths[i];
|
|
75
|
+
if (typeof s === "string" && (p === s || p.indexOf(s + "/") === 0)) return true;
|
|
76
|
+
if (s instanceof RegExp && s.test(p)) return true;
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function _emitAudit(action, outcome, metadata) {
|
|
82
|
+
if (!auditOn) return;
|
|
83
|
+
try {
|
|
84
|
+
audit().safeEmit({
|
|
85
|
+
action: "middleware.age_gate." + action,
|
|
86
|
+
outcome: outcome,
|
|
87
|
+
metadata: metadata || {},
|
|
88
|
+
});
|
|
89
|
+
} catch (_e) { /* drop-silent */ }
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return function ageGateMiddleware(req, res, next) {
|
|
93
|
+
if (_shouldSkip(req)) return next();
|
|
94
|
+
|
|
95
|
+
var age;
|
|
96
|
+
try { age = getAge(req); }
|
|
97
|
+
catch (e) {
|
|
98
|
+
_emitAudit("get_age_failed", "failure", { error: (e && e.message) || String(e) });
|
|
99
|
+
age = null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
var classification;
|
|
103
|
+
if (age === null || typeof age !== "number") classification = "unknown";
|
|
104
|
+
else if (consentRequired !== null && age < consentRequired) classification = "below-threshold";
|
|
105
|
+
else classification = "above-threshold";
|
|
106
|
+
|
|
107
|
+
if (classification !== "above-threshold") {
|
|
108
|
+
if (typeof res.setHeader === "function") {
|
|
109
|
+
res.setHeader("Cache-Control", "private, no-store");
|
|
110
|
+
res.setHeader("Referrer-Policy", "no-referrer");
|
|
111
|
+
res.setHeader("X-Privacy-Posture", classification);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (requireAge !== null && classification === "below-threshold" && (age === null || age < requireAge)) {
|
|
116
|
+
var hasConsent = hasParentalConsent ? !!hasParentalConsent(req) : false;
|
|
117
|
+
if (!hasConsent) {
|
|
118
|
+
_emitAudit("refused", "denied", { age: age, classification: classification, requireAge: requireAge });
|
|
119
|
+
if (!res.writableEnded && typeof res.writeHead === "function") {
|
|
120
|
+
res.writeHead(451, { // allow:raw-byte-literal — HTTP 451 Unavailable For Legal Reasons
|
|
121
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
122
|
+
"Cache-Control": "no-store, private",
|
|
123
|
+
});
|
|
124
|
+
res.end(JSON.stringify({ error: errorMessage, requireAge: requireAge, parentalConsent: false }));
|
|
125
|
+
}
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (req.locals && typeof req.locals === "object") {
|
|
131
|
+
req.locals.ageGateClassification = classification;
|
|
132
|
+
}
|
|
133
|
+
_emitAudit("classified", "success", { classification: classification, age: age });
|
|
134
|
+
return next();
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = {
|
|
139
|
+
create: create,
|
|
140
|
+
AgeGateError: AgeGateError,
|
|
141
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* botDisclose middleware — California SB 1001 bot-disclosure.
|
|
4
|
+
*
|
|
5
|
+
* Cal. Bus. & Prof. Code §17941 (SB 1001, effective 2019) requires
|
|
6
|
+
* that any "bot" used to communicate with persons in California for
|
|
7
|
+
* the purpose of incentivizing a sale or influencing an election
|
|
8
|
+
* disclose its non-human nature. Operators serving an automated
|
|
9
|
+
* conversation surface (LLM-backed chat / IVR / SMS) wire this
|
|
10
|
+
* middleware to:
|
|
11
|
+
*
|
|
12
|
+
* 1. Inject a disclosure banner into HTML responses (server-rendered)
|
|
13
|
+
* 2. Set an X-Bot-Disclosure header for API consumers
|
|
14
|
+
* 3. Emit an audit event for every conversation-initiating request
|
|
15
|
+
*
|
|
16
|
+
* var bot = b.middleware.botDisclose({
|
|
17
|
+
* audit: b.audit,
|
|
18
|
+
* mountPaths: ["/chat", "/api/chat"],
|
|
19
|
+
* bannerHtml: '<div role="status">You are interacting with an automated assistant.</div>',
|
|
20
|
+
* bannerJson: { _bot: true, disclosure: "automated-assistant" },
|
|
21
|
+
* });
|
|
22
|
+
* router.use(bot);
|
|
23
|
+
*
|
|
24
|
+
* Per SB 1001 §17941(a), the disclosure must be "clear, conspicuous,
|
|
25
|
+
* and reasonably designed to inform"; operators with custom UI wire
|
|
26
|
+
* `bannerHtml` to match their visual design.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
var defineClass = require("../framework-error").defineClass;
|
|
30
|
+
var lazyRequire = require("../lazy-require");
|
|
31
|
+
var validateOpts = require("../validate-opts");
|
|
32
|
+
|
|
33
|
+
var audit = lazyRequire(function () { return require("../audit"); });
|
|
34
|
+
|
|
35
|
+
var BotDiscloseError = defineClass("BotDiscloseError", { alwaysPermanent: true });
|
|
36
|
+
|
|
37
|
+
var DEFAULT_BANNER_HTML = '<div role="status" data-bot-disclosure="true" ' +
|
|
38
|
+
'style="border:1px solid #888;padding:8px;margin:8px 0;background:#fff8e1;font-size:14px;">' +
|
|
39
|
+
'<strong>Automated assistant.</strong> ' +
|
|
40
|
+
'You are interacting with an automated agent. ' +
|
|
41
|
+
'For California users: this disclosure is provided per Cal. Bus. & Prof. Code §17941.' +
|
|
42
|
+
'</div>';
|
|
43
|
+
|
|
44
|
+
function create(opts) {
|
|
45
|
+
opts = opts || {};
|
|
46
|
+
validateOpts(opts, [
|
|
47
|
+
"audit", "mountPaths", "bannerHtml", "bannerJson",
|
|
48
|
+
"headerName", "auditAction",
|
|
49
|
+
], "middleware.botDisclose");
|
|
50
|
+
|
|
51
|
+
var mountPaths = Array.isArray(opts.mountPaths) ? opts.mountPaths.slice() : null;
|
|
52
|
+
var bannerHtml = typeof opts.bannerHtml === "string" ? opts.bannerHtml : DEFAULT_BANNER_HTML;
|
|
53
|
+
var bannerJson = (opts.bannerJson && typeof opts.bannerJson === "object")
|
|
54
|
+
? opts.bannerJson : { _bot: true, disclosure: "automated-assistant" };
|
|
55
|
+
var headerName = typeof opts.headerName === "string" && opts.headerName.length > 0
|
|
56
|
+
? opts.headerName : "X-Bot-Disclosure";
|
|
57
|
+
var auditOn = opts.audit !== false;
|
|
58
|
+
var actionBase = typeof opts.auditAction === "string" && opts.auditAction.length > 0
|
|
59
|
+
? opts.auditAction : "middleware.bot_disclose";
|
|
60
|
+
|
|
61
|
+
function _matches(req) {
|
|
62
|
+
if (!mountPaths) return true; // null = match every path
|
|
63
|
+
var p = req.url || "";
|
|
64
|
+
var qpos = p.indexOf("?");
|
|
65
|
+
if (qpos !== -1) p = p.slice(0, qpos);
|
|
66
|
+
for (var i = 0; i < mountPaths.length; i++) {
|
|
67
|
+
var m = mountPaths[i];
|
|
68
|
+
if (typeof m === "string" && (p === m || p.indexOf(m + "/") === 0)) return true;
|
|
69
|
+
if (m instanceof RegExp && m.test(p)) return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _emitAudit(action, outcome, metadata) {
|
|
75
|
+
if (!auditOn) return;
|
|
76
|
+
try {
|
|
77
|
+
audit().safeEmit({
|
|
78
|
+
action: actionBase + "." + action,
|
|
79
|
+
outcome: outcome,
|
|
80
|
+
metadata: metadata || {},
|
|
81
|
+
});
|
|
82
|
+
} catch (_e) { /* drop-silent */ }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return function botDiscloseMiddleware(req, res, next) {
|
|
86
|
+
if (!_matches(req)) return next();
|
|
87
|
+
|
|
88
|
+
// Always set the header (cheap; API consumers see the disclosure
|
|
89
|
+
// even on JSON endpoints).
|
|
90
|
+
if (typeof res.setHeader === "function") {
|
|
91
|
+
res.setHeader(headerName, "automated-assistant");
|
|
92
|
+
}
|
|
93
|
+
_emitAudit("disclosed", "success", { method: req.method, path: req.url });
|
|
94
|
+
|
|
95
|
+
// Patch res.write / res.end so the first text/html response gets
|
|
96
|
+
// the banner injected before <body>'s first child. Operators
|
|
97
|
+
// wanting deeper integration override bannerHtml or set
|
|
98
|
+
// mountPaths to scope where injection happens.
|
|
99
|
+
var origWrite = res.write && res.write.bind(res);
|
|
100
|
+
var origEnd = res.end && res.end.bind(res);
|
|
101
|
+
var injected = false;
|
|
102
|
+
|
|
103
|
+
function _maybeInject(chunk, encoding) {
|
|
104
|
+
if (injected) return chunk;
|
|
105
|
+
var ct = typeof res.getHeader === "function" ? res.getHeader("content-type") : "";
|
|
106
|
+
if (typeof ct !== "string" || ct.indexOf("text/html") === -1) return chunk;
|
|
107
|
+
var body = Buffer.isBuffer(chunk) ? chunk.toString("utf8") :
|
|
108
|
+
(typeof chunk === "string" ? chunk : "");
|
|
109
|
+
// Inject after <body> opening tag if present, else after <html>
|
|
110
|
+
// opening tag, else prepend.
|
|
111
|
+
var bodyMatch = body.match(/<body[^>]*>/i);
|
|
112
|
+
if (bodyMatch) {
|
|
113
|
+
var idx = bodyMatch.index + bodyMatch[0].length;
|
|
114
|
+
body = body.slice(0, idx) + "\n" + bannerHtml + "\n" + body.slice(idx);
|
|
115
|
+
} else {
|
|
116
|
+
body = bannerHtml + "\n" + body;
|
|
117
|
+
}
|
|
118
|
+
injected = true;
|
|
119
|
+
return Buffer.from(body, "utf8");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (origWrite) {
|
|
123
|
+
res.write = function (chunk, encoding, cb) {
|
|
124
|
+
return origWrite(_maybeInject(chunk, encoding), encoding, cb);
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (origEnd) {
|
|
128
|
+
res.end = function (chunk, encoding, cb) {
|
|
129
|
+
if (chunk) chunk = _maybeInject(chunk, encoding);
|
|
130
|
+
return origEnd(chunk, encoding, cb);
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// For JSON responses, attach the disclosure to res.locals so
|
|
135
|
+
// operator handlers building JSON via res.json(...) can include
|
|
136
|
+
// it explicitly. We don't auto-merge into the JSON body — the
|
|
137
|
+
// header + audit are the load-bearing disclosure surfaces.
|
|
138
|
+
if (res.locals && typeof res.locals === "object") {
|
|
139
|
+
res.locals.botDisclosure = bannerJson;
|
|
140
|
+
}
|
|
141
|
+
return next();
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
create: create,
|
|
147
|
+
BotDiscloseError: BotDiscloseError,
|
|
148
|
+
DEFAULT_BANNER_HTML: DEFAULT_BANNER_HTML,
|
|
149
|
+
};
|
package/lib/middleware/index.js
CHANGED
|
@@ -25,6 +25,7 @@ var assetlinks = require("./assetlinks");
|
|
|
25
25
|
var attachUser = require("./attach-user");
|
|
26
26
|
var bearerAuth = require("./bearer-auth");
|
|
27
27
|
var bodyParser = require("./body-parser");
|
|
28
|
+
var botDisclose = require("./bot-disclose");
|
|
28
29
|
var botGuard = require("./bot-guard");
|
|
29
30
|
var compression = require("./compression");
|
|
30
31
|
var cookies = require("./cookies");
|
|
@@ -47,6 +48,7 @@ var requestLog = require("./request-log");
|
|
|
47
48
|
var requireAal = require("./require-aal");
|
|
48
49
|
var requireAuth = require("./require-auth");
|
|
49
50
|
var requireContentType = require("./require-content-type");
|
|
51
|
+
var ageGate = require("./age-gate");
|
|
50
52
|
var requireMethods = require("./require-methods");
|
|
51
53
|
var requireMtls = require("./require-mtls");
|
|
52
54
|
var requireStepUp = require("./require-step-up");
|
|
@@ -63,6 +65,7 @@ module.exports = {
|
|
|
63
65
|
requestId: requestId.create,
|
|
64
66
|
securityHeaders: securityHeaders.create,
|
|
65
67
|
errorHandler: errorHandler.create,
|
|
68
|
+
botDisclose: botDisclose.create,
|
|
66
69
|
botGuard: botGuard.create,
|
|
67
70
|
cors: cors.create,
|
|
68
71
|
dailyByteQuota: dailyByteQuota.create,
|
|
@@ -72,6 +75,7 @@ module.exports = {
|
|
|
72
75
|
requireAal: requireAal.create,
|
|
73
76
|
requireAuth: requireAuth.create,
|
|
74
77
|
requireContentType: requireContentType.create,
|
|
78
|
+
ageGate: ageGate.create,
|
|
75
79
|
requireMethods: requireMethods.create,
|
|
76
80
|
requireMtls: requireMtls.create,
|
|
77
81
|
requireStepUp: requireStepUp.create,
|
|
@@ -108,6 +112,7 @@ module.exports = {
|
|
|
108
112
|
requestId: requestId,
|
|
109
113
|
securityHeaders: securityHeaders,
|
|
110
114
|
errorHandler: errorHandler,
|
|
115
|
+
botDisclose: botDisclose,
|
|
111
116
|
botGuard: botGuard,
|
|
112
117
|
cors: cors,
|
|
113
118
|
dailyByteQuota: dailyByteQuota,
|
|
@@ -117,6 +122,7 @@ module.exports = {
|
|
|
117
122
|
requireAal: requireAal,
|
|
118
123
|
requireAuth: requireAuth,
|
|
119
124
|
requireContentType: requireContentType,
|
|
125
|
+
ageGate: ageGate,
|
|
120
126
|
requireMethods: requireMethods,
|
|
121
127
|
requireMtls: requireMtls,
|
|
122
128
|
requireStepUp: requireStepUp,
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* b.nis2.report — NIS2 Directive incident-reporting wrapper.
|
|
4
|
+
*
|
|
5
|
+
* Directive (EU) 2022/2555 (NIS2) Article 23 mandates that essential
|
|
6
|
+
* + important entities report significant incidents to the national
|
|
7
|
+
* CSIRT or competent authority. Three-stage pattern with statutory
|
|
8
|
+
* deadlines:
|
|
9
|
+
* - early warning within 24h of becoming aware (Art. 23 §4(a))
|
|
10
|
+
* - incident notification within 72h, including initial assessment
|
|
11
|
+
* of severity + impact + indicators of compromise (Art. 23 §4(b))
|
|
12
|
+
* - final report within 1 month, with detailed root cause +
|
|
13
|
+
* remediation + cross-border implications (Art. 23 §4(d))
|
|
14
|
+
*
|
|
15
|
+
* var nis2 = b.nis2.report.create({
|
|
16
|
+
* audit: b.audit,
|
|
17
|
+
* entityId: "acme-cloud-1",
|
|
18
|
+
* entityType: "essential", // "essential" | "important"
|
|
19
|
+
* sectorAnnex: "I.6", // NIS2 Annex I/II row id
|
|
20
|
+
* csirtEndpoint: "https://csirt.example/api",
|
|
21
|
+
* httpClient: b.httpClient,
|
|
22
|
+
* });
|
|
23
|
+
*
|
|
24
|
+
* Sector annex codes follow NIS2's two annexes:
|
|
25
|
+
* - Annex I (essential): I.1 energy / I.2 transport / I.3 banking /
|
|
26
|
+
* I.4 financial-market-infrastructures / I.5 health /
|
|
27
|
+
* I.6 drinking-water / I.7 wastewater / I.8 digital-infrastructure /
|
|
28
|
+
* I.9 ICT-service-management / I.10 public-administration /
|
|
29
|
+
* I.11 space
|
|
30
|
+
* - Annex II (important): II.1 postal / II.2 waste-management /
|
|
31
|
+
* II.3 chemicals / II.4 food / II.5 manufacturing /
|
|
32
|
+
* II.6 digital-providers / II.7 research
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
var C = require("./constants");
|
|
36
|
+
var defineClass = require("./framework-error").defineClass;
|
|
37
|
+
var lazyRequire = require("./lazy-require");
|
|
38
|
+
var validateOpts = require("./validate-opts");
|
|
39
|
+
|
|
40
|
+
var incidentReport = lazyRequire(function () { return require("./incident-report"); });
|
|
41
|
+
var audit = lazyRequire(function () { return require("./audit"); });
|
|
42
|
+
|
|
43
|
+
var Nis2ReportError = defineClass("Nis2ReportError", { alwaysPermanent: true });
|
|
44
|
+
|
|
45
|
+
var VALID_ENTITY_TYPES = Object.freeze({ essential: 1, important: 1 });
|
|
46
|
+
|
|
47
|
+
function create(opts) {
|
|
48
|
+
opts = opts || {};
|
|
49
|
+
validateOpts(opts, [
|
|
50
|
+
"audit", "persist", "httpClient", "csirtEndpoint",
|
|
51
|
+
"entityId", "entityType", "sectorAnnex", "now",
|
|
52
|
+
], "nis2.report");
|
|
53
|
+
|
|
54
|
+
validateOpts.requireNonEmptyString(opts.entityId,
|
|
55
|
+
"nis2.report.create: opts.entityId is required (NIS2 registration ID)",
|
|
56
|
+
Nis2ReportError, "nis2-report/bad-entity-id");
|
|
57
|
+
if (!VALID_ENTITY_TYPES[opts.entityType]) {
|
|
58
|
+
throw new Nis2ReportError("nis2-report/bad-entity-type",
|
|
59
|
+
"nis2.report.create: opts.entityType must be 'essential' or 'important' (NIS2 Article 3 classification)");
|
|
60
|
+
}
|
|
61
|
+
validateOpts.requireNonEmptyString(opts.sectorAnnex,
|
|
62
|
+
"nis2.report.create: opts.sectorAnnex is required (e.g. 'I.6' for drinking water, 'II.6' for digital-providers)",
|
|
63
|
+
Nis2ReportError, "nis2-report/bad-sector");
|
|
64
|
+
var entityId = opts.entityId;
|
|
65
|
+
var entityType = opts.entityType;
|
|
66
|
+
var sectorAnnex = opts.sectorAnnex;
|
|
67
|
+
var csirtEndpoint = opts.csirtEndpoint || null;
|
|
68
|
+
var httpClient = opts.httpClient || null;
|
|
69
|
+
var auditOn = opts.audit !== false;
|
|
70
|
+
|
|
71
|
+
var ir = incidentReport().create({
|
|
72
|
+
audit: opts.audit,
|
|
73
|
+
persist: opts.persist,
|
|
74
|
+
now: opts.now,
|
|
75
|
+
deadlines: {
|
|
76
|
+
initial: C.TIME.hours(24), // NIS2 Art. 23 §4(a) — early warning
|
|
77
|
+
intermediate: C.TIME.hours(72), // NIS2 Art. 23 §4(b) — incident notification
|
|
78
|
+
final: C.TIME.days(30), // NIS2 Art. 23 §4(d) — final report (1 month)
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
function _emitAudit(action, outcome, metadata) {
|
|
83
|
+
if (!auditOn) return;
|
|
84
|
+
try {
|
|
85
|
+
audit().safeEmit({
|
|
86
|
+
action: "nis2.report." + action,
|
|
87
|
+
outcome: outcome,
|
|
88
|
+
metadata: metadata || {},
|
|
89
|
+
});
|
|
90
|
+
} catch (_e) { /* drop-silent */ }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function _submitToCsirt(payload) {
|
|
94
|
+
if (!csirtEndpoint || !httpClient) {
|
|
95
|
+
_emitAudit("submit_skipped", "warning", { reason: "no-endpoint-or-client" });
|
|
96
|
+
return { submitted: false, reason: "no-endpoint-or-client" };
|
|
97
|
+
}
|
|
98
|
+
try {
|
|
99
|
+
var res = await httpClient.request({
|
|
100
|
+
url: csirtEndpoint, method: "POST",
|
|
101
|
+
headers: { "Content-Type": "application/json" },
|
|
102
|
+
body: Buffer.from(JSON.stringify(payload), "utf8"),
|
|
103
|
+
responseMode: "always-resolve",
|
|
104
|
+
});
|
|
105
|
+
var ok = res.statusCode >= 200 && res.statusCode < 300; // allow:raw-byte-literal — HTTP status range
|
|
106
|
+
_emitAudit("submitted", ok ? "success" : "failure", { statusCode: res.statusCode });
|
|
107
|
+
return { submitted: ok, statusCode: res.statusCode };
|
|
108
|
+
} catch (e) {
|
|
109
|
+
_emitAudit("submit_failed", "failure", { error: (e && e.message) || String(e) });
|
|
110
|
+
return { submitted: false, error: (e && e.message) || String(e) };
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function _envelope(stage, incident, fields) {
|
|
115
|
+
return {
|
|
116
|
+
directive: "(EU) 2022/2555",
|
|
117
|
+
article: "23",
|
|
118
|
+
stage: stage, // "early-warning" / "notification" / "final"
|
|
119
|
+
entity: { id: entityId, type: entityType, sector: sectorAnnex },
|
|
120
|
+
incident: {
|
|
121
|
+
id: incident.id,
|
|
122
|
+
detected_at: new Date(incident.detectedAt).toISOString(),
|
|
123
|
+
scope: incident.scope,
|
|
124
|
+
summary: incident.summary,
|
|
125
|
+
impact: incident.impact,
|
|
126
|
+
},
|
|
127
|
+
fields: fields || {},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function open(spec) {
|
|
132
|
+
spec = Object.assign({}, spec || {}, { regime: "nis2" });
|
|
133
|
+
var rec = await ir.open(spec);
|
|
134
|
+
_emitAudit("opened", "success", { incidentId: rec.id, entityId: entityId, entityType: entityType });
|
|
135
|
+
return rec;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function earlyWarning(incidentId, fields) {
|
|
139
|
+
var rec = await ir.recordInitial(incidentId, fields || {});
|
|
140
|
+
var result = { record: rec, submitted: null };
|
|
141
|
+
if (fields && fields.submit === true) {
|
|
142
|
+
result.submitted = await _submitToCsirt(_envelope("early-warning", rec, fields));
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
}
|
|
146
|
+
async function notification(incidentId, fields) {
|
|
147
|
+
var rec = await ir.recordIntermediate(incidentId, fields || {});
|
|
148
|
+
var result = { record: rec, submitted: null };
|
|
149
|
+
if (fields && fields.submit === true) {
|
|
150
|
+
result.submitted = await _submitToCsirt(_envelope("notification", rec, fields));
|
|
151
|
+
}
|
|
152
|
+
return result;
|
|
153
|
+
}
|
|
154
|
+
async function finalReport(incidentId, fields) {
|
|
155
|
+
var rec = await ir.recordFinal(incidentId, fields || {});
|
|
156
|
+
var result = { record: rec, submitted: null };
|
|
157
|
+
if (fields && fields.submit === true) {
|
|
158
|
+
result.submitted = await _submitToCsirt(_envelope("final", rec, fields));
|
|
159
|
+
}
|
|
160
|
+
return result;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
open: open,
|
|
165
|
+
earlyWarning: earlyWarning,
|
|
166
|
+
notification: notification,
|
|
167
|
+
finalReport: finalReport,
|
|
168
|
+
get: function (id) { return ir.get(id); },
|
|
169
|
+
list: function () { return ir.list(); },
|
|
170
|
+
status: function () { return ir.status(); },
|
|
171
|
+
entityId: entityId,
|
|
172
|
+
entityType: entityType,
|
|
173
|
+
sectorAnnex: sectorAnnex,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
module.exports = {
|
|
178
|
+
create: create,
|
|
179
|
+
Nis2ReportError: Nis2ReportError,
|
|
180
|
+
VALID_ENTITY_TYPES: Object.keys(VALID_ENTITY_TYPES),
|
|
181
|
+
};
|
package/package.json
CHANGED
package/sbom.cyclonedx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:ae68f706-2c92-4dfa-b852-f29896f91c62",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-05-
|
|
8
|
+
"timestamp": "2026-05-07T03:30:10.287Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/core@0.8.
|
|
22
|
+
"bom-ref": "@blamejs/core@0.8.11",
|
|
23
23
|
"type": "library",
|
|
24
24
|
"name": "blamejs",
|
|
25
|
-
"version": "0.8.
|
|
25
|
+
"version": "0.8.11",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "The Node framework that owns its stack.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/core@0.8.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/core@0.8.11",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/core@0.8.
|
|
57
|
+
"ref": "@blamejs/core@0.8.11",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|