@chiranthmoger/fortifyjs 1.1.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.
- package/LICENSE +9 -0
- package/README.md +186 -0
- package/bin/fortifyjs.js +135 -0
- package/examples/fastify.js +18 -0
- package/examples/hono.js +13 -0
- package/examples/kitchen-sink.js +50 -0
- package/examples/koa.js +14 -0
- package/examples/minimal-express.js +13 -0
- package/examples/production-express.js +20 -0
- package/index.d.ts +101 -0
- package/package.json +70 -0
- package/src/adapters/express.js +1027 -0
- package/src/adapters/fastify.js +56 -0
- package/src/adapters/generic.js +15 -0
- package/src/adapters/hono.js +77 -0
- package/src/adapters/koa.js +58 -0
- package/src/adapters/nestjs.js +15 -0
- package/src/adapters/nextjs.js +84 -0
- package/src/analyzers/adaptive.js +92 -0
- package/src/analyzers/behavioral.js +264 -0
- package/src/core/confidence.js +23 -0
- package/src/core/engine.js +162 -0
- package/src/core/normalizer.js +149 -0
- package/src/core/whitelist.js +40 -0
- package/src/dashboard/handler.js +307 -0
- package/src/detectors/cmdi.js +82 -0
- package/src/detectors/crlf.js +33 -0
- package/src/detectors/graphql.js +56 -0
- package/src/detectors/hpp.js +38 -0
- package/src/detectors/ldap.js +50 -0
- package/src/detectors/nosqli.js +134 -0
- package/src/detectors/open-redirect.js +42 -0
- package/src/detectors/path-traversal.js +55 -0
- package/src/detectors/prototype-pollution.js +65 -0
- package/src/detectors/sqli.js +447 -0
- package/src/detectors/sqli.js.bak +446 -0
- package/src/detectors/ssrf.js +64 -0
- package/src/detectors/template-injection.js +34 -0
- package/src/detectors/xss.js +191 -0
- package/src/detectors/xxe.js +45 -0
- package/src/forensics/reporter.js +74 -0
- package/src/index.js +132 -0
- package/src/logger.js +110 -0
- package/src/presets.js +183 -0
- package/src/shields/bot-detector.js +88 -0
- package/src/shields/cors.js +95 -0
- package/src/shields/csrf.js +120 -0
- package/src/shields/file-upload.js +160 -0
- package/src/shields/headers.js +99 -0
- package/src/shields/rate-limiter.js +70 -0
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const JS_EXECUTION_SINKS = new Set(['alert', 'confirm', 'prompt', 'eval', 'fetch', 'function', 'settimeout', 'setinterval']);
|
|
4
|
+
const JS_GLOBAL_OBJECTS = new Set(['window', 'globalthis', 'self', 'top', 'parent']);
|
|
5
|
+
function isAsciiLetter(ch) {
|
|
6
|
+
return /[A-Za-z]/.test(ch);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isAsciiDigit(ch) {
|
|
10
|
+
return /[0-9]/.test(ch);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function isSqlWordStart(ch) {
|
|
14
|
+
return isAsciiLetter(ch) || ch === '_' || ch === '$';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function isSqlWordPart(ch) {
|
|
18
|
+
return isSqlWordStart(ch) || isAsciiDigit(ch);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function tokenizeJsFragment(value) {
|
|
22
|
+
const text = String(value);
|
|
23
|
+
const tokens = [];
|
|
24
|
+
let i = 0;
|
|
25
|
+
|
|
26
|
+
while (i < text.length && tokens.length < 800) {
|
|
27
|
+
const ch = text[i];
|
|
28
|
+
if (/\s/.test(ch)) {
|
|
29
|
+
i++;
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
if (isSqlWordStart(ch)) {
|
|
33
|
+
const start = i;
|
|
34
|
+
i++;
|
|
35
|
+
while (i < text.length && isSqlWordPart(text[i])) i++;
|
|
36
|
+
tokens.push({ type: 'word', value: text.slice(start, i).toLowerCase() });
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
40
|
+
const quote = ch;
|
|
41
|
+
let valueText = '';
|
|
42
|
+
i++;
|
|
43
|
+
while (i < text.length) {
|
|
44
|
+
if (text[i] === '\\') {
|
|
45
|
+
valueText += text[i + 1] || '';
|
|
46
|
+
i += 2;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (text[i] === quote) {
|
|
50
|
+
i++;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
valueText += text[i];
|
|
54
|
+
i++;
|
|
55
|
+
}
|
|
56
|
+
tokens.push({ type: 'string', value: valueText.toLowerCase() });
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if ('[]().,:;+-*/%{}='.includes(ch)) {
|
|
60
|
+
tokens.push({ type: 'punct', value: ch });
|
|
61
|
+
}
|
|
62
|
+
i++;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return tokens;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function javascriptUrlBodies(value) {
|
|
69
|
+
const text = String(value);
|
|
70
|
+
const bodies = [];
|
|
71
|
+
const protocol = /javascript\s*:/ig;
|
|
72
|
+
let match;
|
|
73
|
+
|
|
74
|
+
while ((match = protocol.exec(text)) !== null) {
|
|
75
|
+
bodies.push(text.slice(protocol.lastIndex, protocol.lastIndex + 300));
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return bodies;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function hasStructuralJavascriptUrlSink(value) {
|
|
82
|
+
for (const body of javascriptUrlBodies(value)) {
|
|
83
|
+
const tokens = tokenizeJsFragment(body);
|
|
84
|
+
let constructorReferences = 0;
|
|
85
|
+
let hasCall = false;
|
|
86
|
+
|
|
87
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
88
|
+
const token = tokens[i];
|
|
89
|
+
if (token.type === 'punct' && token.value === '(') hasCall = true;
|
|
90
|
+
if ((token.type === 'word' || token.type === 'string') && token.value === 'constructor') constructorReferences++;
|
|
91
|
+
|
|
92
|
+
if (token.type !== 'word') continue;
|
|
93
|
+
if (JS_EXECUTION_SINKS.has(token.value) && tokens.slice(i + 1, i + 4).some(next => next.value === '(')) return true;
|
|
94
|
+
if (token.value === 'document' && tokens.slice(i + 1, i + 3).some(next => next.value === '.')) return true;
|
|
95
|
+
if (JS_GLOBAL_OBJECTS.has(token.value) && tokens.slice(i + 1, i + 3).some(next => next.value === '.' || next.value === '[')) return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (constructorReferences >= 2 && hasCall) return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = {
|
|
105
|
+
name: 'xss',
|
|
106
|
+
label: 'xss',
|
|
107
|
+
getSignals() {
|
|
108
|
+
return [
|
|
109
|
+
{
|
|
110
|
+
id: 'script-tag',
|
|
111
|
+
confidence: 0.85,
|
|
112
|
+
pattern: /<\s*script\b/i
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
id: 'html-event-attribute',
|
|
116
|
+
confidence: 0.75,
|
|
117
|
+
pattern: /<[^>]+\bon\w+\s*=/i
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
id: 'event-handler-payload',
|
|
121
|
+
confidence: 0.75,
|
|
122
|
+
pattern: /\bon\w+\s*=\s*["']?[^"'>]*(?:alert|confirm|prompt|document\.|window\.|eval|fetch|Function\s*\()/i
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
id: 'javascript-url-with-sink',
|
|
126
|
+
confidence: 0.75,
|
|
127
|
+
pattern: /\bjavascript\s*:[\s\S]{0,240}(?:(?:alert|confirm|prompt|eval|fetch|Function|setTimeout|setInterval)\s*\(|document\s*\.|(?:window|globalThis|self|top|parent)\s*(?:\.|\[)|(?:\[\s*["']constructor["']\s*\]\s*){2})/i
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
id: 'javascript-url-structural-sink',
|
|
131
|
+
confidence: 0.75,
|
|
132
|
+
test: hasStructuralJavascriptUrlSink
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'javascript-url-attribute',
|
|
136
|
+
confidence: 0.75,
|
|
137
|
+
pattern: /\b(?:href|src|xlink:href|formaction|action)\s*=\s*["']?\s*javascript\s*:/i
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: 'javascript-url',
|
|
141
|
+
confidence: 0.3,
|
|
142
|
+
pattern: /\bjavascript\s*:/i
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
id: 'dangerous-html-container',
|
|
146
|
+
confidence: 0.7,
|
|
147
|
+
pattern: /<\s*(?:iframe|object|embed|applet)\b/i
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
id: 'srcdoc-html',
|
|
151
|
+
confidence: 0.65,
|
|
152
|
+
pattern: /\bsrcdoc\s*=/i
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'html-data-url',
|
|
156
|
+
confidence: 0.65,
|
|
157
|
+
pattern: /\bdata\s*:\s*text\/html/i
|
|
158
|
+
},
|
|
159
|
+
{
|
|
160
|
+
id: 'svg-data-url',
|
|
161
|
+
confidence: 0.65,
|
|
162
|
+
pattern: /(?:\bdata\s*:\s*image\/svg\+xml|SVG_DATA_URI)/i
|
|
163
|
+
},
|
|
164
|
+
{
|
|
165
|
+
id: 'mathml-xss-container',
|
|
166
|
+
confidence: 0.7,
|
|
167
|
+
pattern: /<\s*(?:math|mtext|mglyph|annotation-xml)\b/i
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
id: 'svg-xss-container',
|
|
171
|
+
confidence: 0.7,
|
|
172
|
+
pattern: /<\s*foreignObject\b/i
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
id: 'css-injection',
|
|
176
|
+
confidence: 0.8,
|
|
177
|
+
pattern: /(?:<\s*style[^>]*>[\s\S]*?|\bstyle\s*=\s*["']?[^"'>]*)(?:expression\s*\(|url\s*\(\s*["']?\s*javascript\s*:)/i
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'autofocus-event-bypass',
|
|
181
|
+
confidence: 0.8,
|
|
182
|
+
pattern: /<\s*[^>]*\bonfocus\b[^>]*\bautofocus\b|<\s*[^>]*\bautofocus\b[^>]*\bonfocus\b/i
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: 'media-error-bypass',
|
|
186
|
+
confidence: 0.8,
|
|
187
|
+
pattern: /<\s*(?:video|audio|picture)[^>]*>[\s\S]*?<\s*source[^>]*\bonerror\s*=/i
|
|
188
|
+
}
|
|
189
|
+
]
|
|
190
|
+
}
|
|
191
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
name: 'xxe',
|
|
5
|
+
label: 'xxe',
|
|
6
|
+
getSignals() {
|
|
7
|
+
return [
|
|
8
|
+
{
|
|
9
|
+
id: 'xml-entity-declaration',
|
|
10
|
+
confidence: 0.85,
|
|
11
|
+
pattern: /<!ENTITY\s+/i
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
id: 'xml-system-entity',
|
|
15
|
+
confidence: 0.90,
|
|
16
|
+
pattern: /<!ENTITY\s+[^>]+SYSTEM\s+["'](?:file|http|https|expect):\/\//i
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: 'xml-public-entity',
|
|
20
|
+
confidence: 0.80,
|
|
21
|
+
pattern: /<!ENTITY\s+[^>]+PUBLIC\s+["']/i
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'xml-parameter-entity',
|
|
25
|
+
confidence: 0.80,
|
|
26
|
+
pattern: /<!ENTITY\s+%\s+[^>]+>/i
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: 'xml-entity-expansion',
|
|
30
|
+
confidence: 0.85,
|
|
31
|
+
pattern: /<!ENTITY\s+[^>]+>\s*<!ENTITY\s+[^>]+&(?:[a-zA-Z0-9_]+);/i
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'xml-doctype',
|
|
35
|
+
confidence: 0.50,
|
|
36
|
+
pattern: /<!DOCTYPE\s+/i
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
id: 'xml-cdata-injection',
|
|
40
|
+
confidence: 0.60,
|
|
41
|
+
pattern: /<!\[CDATA\[.*(?:<script|javascript:|on[a-z]+(?:=>|=)).*]]>/i
|
|
42
|
+
}
|
|
43
|
+
];
|
|
44
|
+
}
|
|
45
|
+
};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
class ForensicsReporter {
|
|
4
|
+
constructor(maxSize = 1000) {
|
|
5
|
+
this.maxSize = maxSize;
|
|
6
|
+
this.buffer = [];
|
|
7
|
+
this.pointer = 0;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
logBlock(details = {}) {
|
|
11
|
+
const {
|
|
12
|
+
ip = 'unknown',
|
|
13
|
+
method = 'UNKNOWN',
|
|
14
|
+
path = '/',
|
|
15
|
+
detector = 'unknown',
|
|
16
|
+
confidence = 0,
|
|
17
|
+
payload = '',
|
|
18
|
+
matchedStrings = []
|
|
19
|
+
} = details;
|
|
20
|
+
|
|
21
|
+
let sanitizedPayload = typeof payload === 'string' ? payload : String(payload);
|
|
22
|
+
|
|
23
|
+
if (Array.isArray(matchedStrings) && matchedStrings.length > 0) {
|
|
24
|
+
matchedStrings.forEach(str => {
|
|
25
|
+
if (str && typeof str === 'string') {
|
|
26
|
+
sanitizedPayload = sanitizedPayload.split(str).join('*'.repeat(str.length));
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
} else {
|
|
30
|
+
// Basic fallback masking if no specific matched strings provided but payload contains typical patterns
|
|
31
|
+
sanitizedPayload = sanitizedPayload.replace(/(<script>|UNION SELECT|DROP TABLE|--|;)/gi, '***');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// truncate to 200 chars
|
|
35
|
+
if (sanitizedPayload.length > 200) {
|
|
36
|
+
sanitizedPayload = sanitizedPayload.substring(0, 200) + '...';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const entry = {
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
ip,
|
|
42
|
+
method,
|
|
43
|
+
path,
|
|
44
|
+
detector,
|
|
45
|
+
confidence,
|
|
46
|
+
payloadPreview: sanitizedPayload
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
if (this.buffer.length < this.maxSize) {
|
|
50
|
+
this.buffer.push(entry);
|
|
51
|
+
} else {
|
|
52
|
+
this.buffer[this.pointer] = entry;
|
|
53
|
+
this.pointer = (this.pointer + 1) % this.maxSize;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
getReport() {
|
|
58
|
+
if (this.buffer.length < this.maxSize) {
|
|
59
|
+
return [...this.buffer];
|
|
60
|
+
}
|
|
61
|
+
const result = [];
|
|
62
|
+
for (let i = 0; i < this.maxSize; i++) {
|
|
63
|
+
result.push(this.buffer[(this.pointer + i) % this.maxSize]);
|
|
64
|
+
}
|
|
65
|
+
return result;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
clear() {
|
|
69
|
+
this.buffer = [];
|
|
70
|
+
this.pointer = 0;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = { ForensicsReporter };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
const { shield } = require('./presets');
|
|
3
|
+
const { DetectionEngine } = require('./core/engine');
|
|
4
|
+
const { Normalizer } = require('./core/normalizer');
|
|
5
|
+
const { expressMiddleware, fortifyjs, secureRouter } = require('./adapters/express');
|
|
6
|
+
const { createNestMiddleware, nestjsMiddleware } = require('./adapters/nestjs');
|
|
7
|
+
const fastifyPlugin = require('./adapters/fastify');
|
|
8
|
+
const { koaMiddleware } = require('./adapters/koa');
|
|
9
|
+
const { honoMiddleware } = require('./adapters/hono');
|
|
10
|
+
const { genericAdapter } = require('./adapters/generic');
|
|
11
|
+
const nextjsAdapter = require('./adapters/nextjs');
|
|
12
|
+
const DEFAULT_THRESHOLD = 0.5;
|
|
13
|
+
|
|
14
|
+
function samplePayload(sample) {
|
|
15
|
+
if (typeof sample === 'string') return sample;
|
|
16
|
+
if (sample && typeof sample === 'object') {
|
|
17
|
+
return sample.payload !== undefined ? sample.payload : (sample.query !== undefined ? sample.query : JSON.stringify(sample));
|
|
18
|
+
}
|
|
19
|
+
return String(sample);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function expectedMaliciousLabel(label) {
|
|
23
|
+
if (typeof label === 'boolean') return label;
|
|
24
|
+
const str = String(label).toLowerCase();
|
|
25
|
+
return str !== 'benign' && str !== 'safe' && str !== 'false' && str !== '0';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function resolveDetectionSettings(options) {
|
|
29
|
+
const threshold = typeof options.threshold === 'number' ? options.threshold : DEFAULT_THRESHOLD;
|
|
30
|
+
return { threshold };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class fortifyjsQueryError extends Error {
|
|
34
|
+
constructor(result) {
|
|
35
|
+
super(`fortifyjs detected a SQL injection attempt (confidence: ${result.confidence.toFixed(2)})`);
|
|
36
|
+
this.name = 'fortifyjsQueryError';
|
|
37
|
+
this.result = result;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function scanSqlQuery(query, options = {}) {
|
|
42
|
+
if (typeof query !== 'string') {
|
|
43
|
+
throw new TypeError('query must be a string');
|
|
44
|
+
}
|
|
45
|
+
const detector = options.detector || new DetectionEngine({
|
|
46
|
+
maxPayloadLength: options.maxPayloadLength,
|
|
47
|
+
maxDecodeIterations: options.maxDecodeIterations
|
|
48
|
+
});
|
|
49
|
+
return detector.detect(query);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function assertSafeSqlQuery(query, options = {}) {
|
|
53
|
+
const result = scanSqlQuery(query, options);
|
|
54
|
+
const { threshold } = resolveDetectionSettings(options);
|
|
55
|
+
if (result.label === 'sqli' && result.confidence >= threshold) {
|
|
56
|
+
throw new fortifyjsQueryError(result);
|
|
57
|
+
}
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
60
|
+
function evaluatePayloads(samples, options = {}) {
|
|
61
|
+
if (!Array.isArray(samples)) {
|
|
62
|
+
throw new TypeError('samples must be an array');
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const detector = options.detector || new DetectionEngine({
|
|
66
|
+
maxPayloadLength: options.maxPayloadLength,
|
|
67
|
+
maxDecodeIterations: options.maxDecodeIterations
|
|
68
|
+
});
|
|
69
|
+
const { threshold } = resolveDetectionSettings(options);
|
|
70
|
+
const results = [];
|
|
71
|
+
const summary = {
|
|
72
|
+
total: samples.length,
|
|
73
|
+
blocked: 0,
|
|
74
|
+
allowed: 0,
|
|
75
|
+
labeled: 0,
|
|
76
|
+
falsePositives: 0,
|
|
77
|
+
falseNegatives: 0,
|
|
78
|
+
truePositives: 0,
|
|
79
|
+
trueNegatives: 0,
|
|
80
|
+
falsePositiveRate: 0,
|
|
81
|
+
falseNegativeRate: 0
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
for (const sample of samples) {
|
|
85
|
+
const payload = String(samplePayload(sample));
|
|
86
|
+
const expectedMalicious = typeof sample === 'object' && sample !== null
|
|
87
|
+
? expectedMaliciousLabel(sample.label ?? sample.expected ?? sample.kind)
|
|
88
|
+
: null;
|
|
89
|
+
const result = detector.detect(payload);
|
|
90
|
+
const blocked = result.label !== 'benign' && result.confidence >= threshold;
|
|
91
|
+
if (blocked) summary.blocked++;
|
|
92
|
+
else summary.allowed++;
|
|
93
|
+
|
|
94
|
+
if (expectedMalicious !== null) {
|
|
95
|
+
summary.labeled++;
|
|
96
|
+
if (blocked && expectedMalicious) summary.truePositives++;
|
|
97
|
+
else if (blocked && !expectedMalicious) summary.falsePositives++;
|
|
98
|
+
else if (!blocked && expectedMalicious) summary.falseNegatives++;
|
|
99
|
+
else summary.trueNegatives++;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
results.push({ payload, expectedMalicious, blocked, result });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const benignCount = summary.trueNegatives + summary.falsePositives;
|
|
106
|
+
const maliciousCount = summary.truePositives + summary.falseNegatives;
|
|
107
|
+
summary.falsePositiveRate = benignCount === 0 ? 0 : summary.falsePositives / benignCount;
|
|
108
|
+
summary.falseNegativeRate = maliciousCount === 0 ? 0 : summary.falseNegatives / maliciousCount;
|
|
109
|
+
|
|
110
|
+
return { threshold, summary, results };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
module.exports = {
|
|
114
|
+
shield,
|
|
115
|
+
DetectionEngine,
|
|
116
|
+
Normalizer,
|
|
117
|
+
evaluatePayloads,
|
|
118
|
+
fortifyjsQueryError,
|
|
119
|
+
expressMiddleware,
|
|
120
|
+
createNestMiddleware,
|
|
121
|
+
fortifyjs,
|
|
122
|
+
secureRouter,
|
|
123
|
+
nestjsMiddleware,
|
|
124
|
+
scanSqlQuery,
|
|
125
|
+
assertSafeSqlQuery,
|
|
126
|
+
evaluatePayloads,
|
|
127
|
+
fastifyPlugin,
|
|
128
|
+
koaMiddleware,
|
|
129
|
+
honoMiddleware,
|
|
130
|
+
genericAdapter,
|
|
131
|
+
nextjsAdapter
|
|
132
|
+
};
|
package/src/logger.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const crypto = require('crypto');
|
|
6
|
+
|
|
7
|
+
const LOG_LEVELS = {
|
|
8
|
+
silent: 0,
|
|
9
|
+
error: 1,
|
|
10
|
+
warn: 2,
|
|
11
|
+
info: 3,
|
|
12
|
+
debug: 4
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
class Logger {
|
|
16
|
+
constructor(options = {}) {
|
|
17
|
+
this.level = LOG_LEVELS[options.level || 'info'];
|
|
18
|
+
this.format = options.format || 'json'; // json, text
|
|
19
|
+
this.maxLogs = options.maxLogs || 500;
|
|
20
|
+
this.transport = options.transport || null;
|
|
21
|
+
|
|
22
|
+
// Rate limiting: max N logs per second
|
|
23
|
+
this.rateLimit = options.rateLimit || { max: 100, windowMs: 1000 };
|
|
24
|
+
this.logCounts = new Map();
|
|
25
|
+
|
|
26
|
+
this.inMemoryLogs = [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
_checkRateLimit(category) {
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
const windowStart = now - this.rateLimit.windowMs;
|
|
32
|
+
|
|
33
|
+
// Cleanup old entries
|
|
34
|
+
for (const [key, data] of this.logCounts.entries()) {
|
|
35
|
+
if (data.timestamp < windowStart) {
|
|
36
|
+
this.logCounts.delete(key);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let record = this.logCounts.get(category);
|
|
41
|
+
if (!record) {
|
|
42
|
+
record = { count: 0, timestamp: now };
|
|
43
|
+
this.logCounts.set(category, record);
|
|
44
|
+
} else if (record.timestamp < windowStart) {
|
|
45
|
+
record.count = 0;
|
|
46
|
+
record.timestamp = now;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
record.count++;
|
|
50
|
+
return record.count <= this.rateLimit.max;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
log(levelName, message, meta = {}) {
|
|
54
|
+
const levelVal = LOG_LEVELS[levelName];
|
|
55
|
+
if (this.level < levelVal || this.level === 0) return;
|
|
56
|
+
|
|
57
|
+
if (!this._checkRateLimit('global')) {
|
|
58
|
+
return; // Rate limited
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const event = {
|
|
62
|
+
timestamp: new Date().toISOString(),
|
|
63
|
+
id: crypto.randomBytes(8).toString('hex'),
|
|
64
|
+
level: levelName,
|
|
65
|
+
message,
|
|
66
|
+
...meta
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
this.inMemoryLogs.unshift(event);
|
|
70
|
+
if (this.inMemoryLogs.length > this.maxLogs) {
|
|
71
|
+
this.inMemoryLogs.pop();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (this.transport && typeof this.transport === 'function') {
|
|
75
|
+
try {
|
|
76
|
+
this.transport(event);
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// ignore transport errors
|
|
79
|
+
}
|
|
80
|
+
} else if (!this.transport && this.format === 'json') {
|
|
81
|
+
if (levelName === 'error') {
|
|
82
|
+
console.error(JSON.stringify(event));
|
|
83
|
+
} else if (levelName === 'warn') {
|
|
84
|
+
console.warn(JSON.stringify(event));
|
|
85
|
+
} else {
|
|
86
|
+
console.log(JSON.stringify(event));
|
|
87
|
+
}
|
|
88
|
+
} else if (!this.transport && this.format === 'text') {
|
|
89
|
+
const msg = `[${event.timestamp}] ${levelName.toUpperCase()}: ${message} ${Object.keys(meta).length ? JSON.stringify(meta) : ''}`;
|
|
90
|
+
if (levelName === 'error') console.error(msg);
|
|
91
|
+
else if (levelName === 'warn') console.warn(msg);
|
|
92
|
+
else console.log(msg);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
error(message, meta) { this.log('error', message, meta); }
|
|
97
|
+
warn(message, meta) { this.log('warn', message, meta); }
|
|
98
|
+
info(message, meta) { this.log('info', message, meta); }
|
|
99
|
+
debug(message, meta) { this.log('debug', message, meta); }
|
|
100
|
+
|
|
101
|
+
getLogs(limit = 100) {
|
|
102
|
+
return this.inMemoryLogs.slice(0, limit);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
clearLogs() {
|
|
106
|
+
this.inMemoryLogs = [];
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module.exports = { Logger };
|