@contrast/reporter
Advanced tools
Comparing version 1.1.0 to 1.2.0
@@ -19,3 +19,7 @@ "use strict"; | ||
const types_1 = require("./types"); | ||
const serverFeaturesRules = ['virtual-patch', 'ip-denylist']; | ||
const mapInputType = (result) => { | ||
/* c8 ignore next 31 */ | ||
if (result.inputType in types_1.InputType) | ||
return result.inputType; | ||
switch (result.inputType) { | ||
@@ -48,8 +52,12 @@ case 'UriPath': | ||
return types_1.InputType.XML_VALUE; | ||
case 'Unknown': | ||
return types_1.InputType.UNKNOWN; | ||
} | ||
}; | ||
const buildInputPayload = (result, time) => ({ | ||
name: result.key, | ||
filters: result.mongoExpansionResult ? ['nosql-expansion'] : [], | ||
name: result.key || '', | ||
time, | ||
type: mapInputType(result), | ||
type: mapInputType(result) || 'UNKNOWN', | ||
// NOTE: In v4 we have other documentTypes too, why pick only NORMAL? | ||
documentType: types_1.DocumentType.NORMAL, | ||
@@ -60,4 +68,15 @@ value: result.value, | ||
const reflectedXSSDetailsBuilder = (el) => ({}); // TODO | ||
const ssjsDetailsBuilder = (el) => ({}); // TODO | ||
function default_1() { | ||
const ssjsDetailsBuilder = (el) => { | ||
if (!el.details || el.details.length === 0) { | ||
return {}; | ||
} | ||
const { findings } = el.details[0]; | ||
return { | ||
start: findings.startIndex, | ||
end: findings.endIndex, | ||
boundaryOverrunIndex: findings.boundaryIndex, | ||
codeString: findings.codeString, | ||
}; | ||
}; | ||
const sqlInjectionDetailsBuilder = (el) => { | ||
@@ -76,2 +95,15 @@ if (!el.details || el.details.length === 0) { | ||
}; | ||
const nosqliMongoDetailsBuilder = (el) => { | ||
if (!el.details || el.details.length === 0) { | ||
return {}; | ||
} | ||
const { findings: { start, end, boundaryOverrunIndex, inputBoundaryIndex }, sinkContext } = el.details[0]; | ||
return { | ||
start, | ||
end, | ||
boundaryOverrunIndex, | ||
inputBoundaryIndex, | ||
query: typeof sinkContext.value === 'string' ? sinkContext.value : JSON.stringify(sinkContext.value), | ||
}; | ||
}; | ||
const cmdInjectionDetailsBuilder = (el) => { | ||
@@ -91,2 +123,17 @@ if (!el.details || el.details.length === 0) { | ||
}); | ||
const cmdInjectionSemanticAnalysisDetailsBuilder = (el) => { | ||
const ruleId = el.ruleId; | ||
const ruleIdMap = { | ||
[common_1.Rule.CMD_INJECTION_SEMANTIC_CHAINED_COMMANDS]: 0, | ||
[common_1.Rule.CMD_INJECTION_COMMAND_BACKDOORS]: 1, | ||
[common_1.Rule.CMD_INJECTION_SEMANTIC_DANGEROUS_PATHS]: 2 | ||
}; | ||
return { | ||
command: el.value, | ||
findings: [ruleIdMap[ruleId]] | ||
}; | ||
}; | ||
const untrustedDeserializationDetailsBuilder = (el) => el.details?.[0]; | ||
const virtualPatchDetailsBuilder = (el) => el.details?.[0] || {}; | ||
const ipDenylistDetailsBuilder = (el) => el.details?.[0] || {}; | ||
const buildRequestObject = (reqData) => { | ||
@@ -130,2 +177,5 @@ const searchParams = new URLSearchParams(reqData.queries); | ||
: null; | ||
if (result.ruleId === common_1.Rule.NOSQL_INJECTION_MONGO && typeof result.value !== 'string') { | ||
result.mongoExpansionResult = true; | ||
} | ||
const data = { | ||
@@ -145,3 +195,3 @@ details: detailsBuilder(result), | ||
if (result.blocked) { | ||
if (detail) { | ||
if (detail && !serverFeaturesRules.includes(result.ruleId)) { | ||
accumulator.blocked.total += 1; | ||
@@ -223,2 +273,110 @@ accumulator.blocked.samples.push(data); | ||
} | ||
const nosqlInjectionMongo = protect.findings.resultsMap[common_1.Rule.NOSQL_INJECTION_MONGO]; | ||
if (nosqlInjectionMongo) { | ||
const isBlockMode = protect.rules.agentLibRules[common_1.Rule.NOSQL_INJECTION_MONGO].mode === 'block'; | ||
const protectionRules = buildProtectionRules(nosqlInjectionMongo, requestPayload, time, isBlockMode, nosqliMongoDetailsBuilder); | ||
if (protectionRules) { | ||
defendObject.protectionRules[common_1.Rule.NOSQL_INJECTION] = protectionRules; | ||
hasAttack = true; | ||
} | ||
} | ||
const cmdiSemanticAnalysisDangerousPaths = protect.findings.semanticResultsMap[common_1.Rule.CMD_INJECTION_SEMANTIC_DANGEROUS_PATHS]; | ||
if (cmdiSemanticAnalysisDangerousPaths) { | ||
const isBlockMode = protect.rules.agentRules[common_1.Rule.CMD_INJECTION_SEMANTIC_DANGEROUS_PATHS].mode === 'block'; | ||
cmdiSemanticAnalysisDangerousPaths.forEach((vulnerability) => { | ||
Object.assign(vulnerability, { | ||
inputType: 'Unknown', | ||
key: 'Unknown', | ||
value: vulnerability.findings?.command, | ||
ruleId: common_1.Rule.CMD_INJECTION_SEMANTIC_DANGEROUS_PATHS, | ||
details: [{ ...vulnerability.findings }] | ||
}); | ||
}); | ||
const protectionRules = buildProtectionRules(cmdiSemanticAnalysisDangerousPaths, requestPayload, time, isBlockMode, cmdInjectionSemanticAnalysisDetailsBuilder); | ||
if (protectionRules) { | ||
defendObject.protectionRules[common_1.Rule.CMD_INJECTION_SEMANTIC_DANGEROUS_PATHS] = protectionRules; | ||
hasAttack = true; | ||
} | ||
} | ||
const cmdiSemanticAnalysisChainedCommands = protect.findings.semanticResultsMap[common_1.Rule.CMD_INJECTION_SEMANTIC_CHAINED_COMMANDS]; | ||
if (cmdiSemanticAnalysisChainedCommands) { | ||
const isBlockMode = protect.rules.agentRules[common_1.Rule.CMD_INJECTION_SEMANTIC_CHAINED_COMMANDS].mode === 'block'; | ||
cmdiSemanticAnalysisChainedCommands.forEach((vulnerability) => { | ||
Object.assign(vulnerability, { | ||
inputType: 'Unknown', | ||
key: 'Unknown', | ||
value: vulnerability.findings?.command, | ||
ruleId: common_1.Rule.CMD_INJECTION_SEMANTIC_CHAINED_COMMANDS, | ||
details: [{ ...vulnerability.findings }] | ||
}); | ||
}); | ||
const protectionRules = buildProtectionRules(cmdiSemanticAnalysisChainedCommands, requestPayload, time, isBlockMode, cmdInjectionSemanticAnalysisDetailsBuilder); | ||
if (protectionRules) { | ||
defendObject.protectionRules[common_1.Rule.CMD_INJECTION_SEMANTIC_CHAINED_COMMANDS] = protectionRules; | ||
hasAttack = true; | ||
} | ||
} | ||
const cmdiCommandBackdoors = protect.findings.semanticResultsMap[common_1.Rule.CMD_INJECTION_COMMAND_BACKDOORS]; | ||
if (cmdiCommandBackdoors) { | ||
const isBlockMode = protect.rules.agentRules[common_1.Rule.CMD_INJECTION_COMMAND_BACKDOORS].mode === 'block'; | ||
cmdiCommandBackdoors.forEach((vulnerability) => { | ||
Object.assign(vulnerability, { | ||
ruleId: common_1.Rule.CMD_INJECTION_COMMAND_BACKDOORS, | ||
details: [{ ...vulnerability.findings }], | ||
}); | ||
}); | ||
const protectionRules = buildProtectionRules(cmdiCommandBackdoors, requestPayload, time, isBlockMode, cmdInjectionSemanticAnalysisDetailsBuilder); | ||
if (protectionRules) { | ||
defendObject.protectionRules[common_1.Rule.CMD_INJECTION_COMMAND_BACKDOORS] = protectionRules; | ||
hasAttack = true; | ||
} | ||
} | ||
const untrustedDeserialization = protect.findings.hardeningResultsMap[common_1.Rule.UNTRUSTED_DESERIALIZATION]; | ||
if (untrustedDeserialization) { | ||
const isBlockMode = protect.rules.agentRules[common_1.Rule.UNTRUSTED_DESERIALIZATION].mode === 'block'; | ||
untrustedDeserialization.forEach((vulnerability) => { | ||
Object.assign(vulnerability, { | ||
ruleId: common_1.Rule.UNTRUSTED_DESERIALIZATION, | ||
value: vulnerability.sinkContext.value, | ||
details: [{ ...vulnerability.findings }], | ||
}); | ||
}); | ||
const protectionRules = buildProtectionRules(untrustedDeserialization, requestPayload, time, isBlockMode, untrustedDeserializationDetailsBuilder); | ||
if (protectionRules) { | ||
defendObject.protectionRules[common_1.Rule.UNTRUSTED_DESERIALIZATION] = protectionRules; | ||
hasAttack = true; | ||
} | ||
} | ||
const virtualPatch = protect.findings.serverFeaturesResultsMap[common_1.Rule.VIRTUAL_PATCH]; | ||
if (virtualPatch) { | ||
const mappedVirtualPatchResults = virtualPatch.map((vulnerability) => ({ | ||
key: vulnerability.name, | ||
inputType: 'UNKNOWN', | ||
ruleId: common_1.Rule.VIRTUAL_PATCH, | ||
value: 'Virtual Patch', | ||
details: [{ uuid: vulnerability.uuid }], | ||
blocked: true | ||
})); | ||
const protectionRules = buildProtectionRules(mappedVirtualPatchResults, requestPayload, time, true, virtualPatchDetailsBuilder); | ||
if (protectionRules) { | ||
defendObject.protectionRules[common_1.Rule.VIRTUAL_PATCH] = protectionRules; | ||
hasAttack = true; | ||
} | ||
} | ||
const ipDenylist = protect.findings.serverFeaturesResultsMap[common_1.Rule.IP_DENYLIST]; | ||
if (ipDenylist) { | ||
const mappedIpDenylist = ipDenylist.map((vulnerability) => ({ | ||
key: 'IP Address', | ||
inputType: 'UNKNOWN', | ||
ruleId: common_1.Rule.IP_DENYLIST, | ||
value: vulnerability.ip, | ||
details: [{ uuid: vulnerability.uuid, ip: vulnerability.ip }], | ||
blocked: true | ||
})); | ||
const protectionRules = buildProtectionRules(mappedIpDenylist, requestPayload, time, true, ipDenylistDetailsBuilder); | ||
if (protectionRules) { | ||
defendObject.protectionRules['ip-blacklist'] = protectionRules; | ||
hasAttack = true; | ||
} | ||
} | ||
return hasAttack ? defendObject : null; | ||
@@ -225,0 +383,0 @@ }; |
/// <reference types="node" /> | ||
import { RequestStore } from '@contrast/common'; | ||
import { RequestStore, Messages } from '@contrast/common'; | ||
import { AxiosInstance } from 'axios'; | ||
@@ -16,2 +16,3 @@ import { Core, AppInfo } from '@contrast/core'; | ||
lastUpdate: number; | ||
lastServerUpdate: number; | ||
activityLoop: NodeJS.Timeout | null; | ||
@@ -21,2 +22,3 @@ httpClient: AxiosInstance; | ||
scopes: Scopes; | ||
messages: Messages; | ||
userAgentSet: Set<string>; | ||
@@ -29,2 +31,3 @@ defendPayload: AttackModel[]; | ||
private applicationActivity; | ||
private serverActivity; | ||
constructor(core: Core, name?: string, appActivityBuilder?: { | ||
@@ -38,4 +41,5 @@ handleProtectMessage(protectMsg: import("@contrast/common").ProtectMessage): { | ||
private initActivityLoop; | ||
private serverSettingsUpdate; | ||
handleAssessEvent(msg: RequestStore): void; | ||
handleProtectEvent(msg: RequestStore): void; | ||
} |
@@ -29,2 +29,3 @@ "use strict"; | ||
this.lastUpdate = 0; | ||
this.lastServerUpdate = 0; | ||
this.activityLoop = null; | ||
@@ -37,2 +38,3 @@ this.userAgentSet = new Set(); | ||
this.scopes = core.scopes; | ||
this.messages = core.messages; | ||
this.httpClient = axios_1.default.create({ | ||
@@ -122,9 +124,29 @@ baseURL: new URL(`${this.config.api.url}`).href, | ||
} | ||
serverActivity(serverActivityInMs) { | ||
return this.httpClient | ||
.put('/api/ng/activity/server', { | ||
lastUpdate: this.lastServerUpdate + serverActivityInMs, | ||
}, { | ||
validateStatus(status) { | ||
return status < 400; | ||
}, | ||
}) | ||
.catch((error) => { | ||
this.logger.error(error, 'Failed to update the server activity!'); | ||
throw error; | ||
}); | ||
} | ||
init() { | ||
return this.startupServer() // settings from TS | ||
.then((payload) => { | ||
this.serverSettingsUpdate(payload); | ||
}) | ||
.then(() => this.createApplication()) | ||
.then((payload) => { | ||
this.serverSettingsUpdate(payload); | ||
}) | ||
.then(() => this.initActivityLoop()); | ||
} | ||
initActivityLoop() { | ||
const appActivityInMs = this.config.agent.polling.app_activity_ms; | ||
const activityIntervalInMs = this.config.agent.polling.app_activity_ms; | ||
this.activityLoop = setInterval(() => { | ||
@@ -135,5 +157,11 @@ const browsers = Array.from(this.userAgentSet.values()); | ||
this.defendPayload = []; | ||
this.applicationActivity({ browsers }, { attackers }, appActivityInMs) | ||
.then(() => { | ||
this.lastUpdate += appActivityInMs; | ||
this.applicationActivity({ browsers }, { attackers }, activityIntervalInMs) | ||
.then((payload) => { | ||
if (payload?.data) { | ||
this.serverSettingsUpdate(payload); | ||
this.lastUpdate = 0; | ||
} | ||
else { | ||
this.lastUpdate += activityIntervalInMs; | ||
} | ||
}) | ||
@@ -147,4 +175,27 @@ .catch((error) => { | ||
}); | ||
}, appActivityInMs).unref(); | ||
this.serverActivity(activityIntervalInMs) | ||
.then((payload) => { | ||
if (payload?.data) { | ||
this.serverSettingsUpdate(payload); | ||
this.lastServerUpdate = 0; | ||
} | ||
else { | ||
this.lastServerUpdate += activityIntervalInMs; | ||
} | ||
}) | ||
.catch((error) => { | ||
this.logger.error({ | ||
error, | ||
}, 'Failed acquiring server activity!'); | ||
}); | ||
}, activityIntervalInMs).unref(); | ||
} | ||
serverSettingsUpdate(serverPayload) { | ||
// Ivaylo: IMO here we can plug in the code for dynamic rule mode update | ||
// for now I'll not include it in this ticket(NODE-2503) | ||
const serverUpdate = serverPayload?.data; | ||
if (serverUpdate) { | ||
this.messages.emit(common_1.Event.SERVER_SETTINGS_UPDATE, serverUpdate); | ||
} | ||
} | ||
/* c8 ignore next 3 */ | ||
@@ -151,0 +202,0 @@ handleAssessEvent(msg) { |
@@ -70,3 +70,10 @@ import { Rule } from '@contrast/common'; | ||
[Rule.SQL_INJECTION]?: SQLInjection; | ||
[Rule.NOSQL_INJECTION]?: NoSQLInjection; | ||
[Rule.SSJS_INJECTION]?: SSJSInjection; | ||
[Rule.CMD_INJECTION_SEMANTIC_DANGEROUS_PATHS]?: CMDInjectionSemanticAnalysis; | ||
[Rule.CMD_INJECTION_SEMANTIC_CHAINED_COMMANDS]?: CMDInjectionSemanticAnalysis; | ||
[Rule.CMD_INJECTION_COMMAND_BACKDOORS]?: CMDInjectionSemanticAnalysis; | ||
[Rule.UNTRUSTED_DESERIALIZATION]?: UntrustedDeserialization; | ||
[Rule.VIRTUAL_PATCH]?: VirtualPatch; | ||
'ip-blacklist'?: IpDenylist; | ||
}; | ||
@@ -94,5 +101,40 @@ } | ||
} | ||
export interface CMDInjectionSemanticAnalysisDetails { | ||
command: string; | ||
findings: number[]; | ||
} | ||
export interface UntrustedDeserializationDetails { | ||
command: boolean; | ||
deserializer: string; | ||
} | ||
export interface CMDInjectionSemanticAnalysis { | ||
startTime: number; | ||
exploited?: AttackBody<DefaultSample<CMDInjectionSemanticAnalysisDetails>>; | ||
blocked?: AttackBody<DefaultSample<CMDInjectionSemanticAnalysisDetails>>; | ||
blockedAtPerimeter?: AttackBody<DefaultSample<CMDInjectionSemanticAnalysisDetails>>; | ||
} | ||
export interface UntrustedDeserialization { | ||
startTime: number; | ||
exploited?: AttackBody<DefaultSample<UntrustedDeserializationDetails>>; | ||
blocked?: AttackBody<DefaultSample<UntrustedDeserializationDetails>>; | ||
blockedAtPerimeter?: AttackBody<DefaultSample<UntrustedDeserializationDetails>>; | ||
} | ||
export interface VirtualPatch { | ||
startTime: number; | ||
blockedAtPerimeter?: AttackBody<DefaultSample<VirtualPatchDetails>>; | ||
} | ||
export interface IpDenylist { | ||
startTime: number; | ||
blockedAtPerimeter?: AttackBody<DefaultSample<IpDenylistDetails>>; | ||
} | ||
export interface PathTraversalDetails { | ||
path: string; | ||
} | ||
export interface VirtualPatchDetails { | ||
uuid?: string; | ||
} | ||
export interface IpDenylistDetails { | ||
uuid?: string; | ||
ip?: string; | ||
} | ||
export interface PathTraversal { | ||
@@ -123,2 +165,9 @@ startTime: number; | ||
} | ||
export interface NoSQLInjectionDetails { | ||
start: number; | ||
end: number; | ||
boundaryOverrunIndex: number; | ||
inputBoundaryIndex: number; | ||
query: string; | ||
} | ||
export interface SQLInjection { | ||
@@ -129,2 +178,7 @@ startTime: number; | ||
} | ||
export interface NoSQLInjection { | ||
startTime: number; | ||
exploited?: AttackBody<DefaultSample<NoSQLInjectionDetails>>; | ||
blocked?: AttackBody<DefaultSample<NoSQLInjectionDetails>>; | ||
} | ||
export interface SSJSInjection { | ||
@@ -131,0 +185,0 @@ startTime: number; |
{ | ||
"name": "@contrast/reporter", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"description": "Subscribes to agent messages and reports them", | ||
@@ -21,3 +21,3 @@ "license": "SEE LICENSE IN LICENSE", | ||
"dependencies": { | ||
"@contrast/common": "1.0.3", | ||
"@contrast/common": "1.1.0", | ||
"axios": "^0.27.2", | ||
@@ -24,0 +24,0 @@ "safe-stable-stringify": "^2.3.1", |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
68411
1040
0
+ Added@contrast/common@1.1.0(transitive)
- Removed@contrast/common@1.0.3(transitive)
Updated@contrast/common@1.1.0