@contrast/protect
Advanced tools
Comparing version 1.4.0 to 1.5.0
@@ -21,10 +21,13 @@ /* | ||
require('./install/fastify3')(core); | ||
require('./install/fastify')(core); | ||
require('./install/koa2')(core); | ||
require('./install/express4')(core); | ||
require('./install/hapi')(core); | ||
errorHandlers.install = function() { | ||
errorHandlers.fastify3ErrorHandler.install(); | ||
errorHandlers.koa2ErrorHandler.install(); | ||
errorHandlers.express4ErrorHandler.install(); | ||
for (const component of Object.values(errorHandlers)) { | ||
if (component.install) { | ||
component.install(); | ||
} | ||
} | ||
}; | ||
@@ -31,0 +34,0 @@ |
@@ -40,3 +40,3 @@ /* | ||
const ruleId = 'untrusted-deserialization'; | ||
const { mode } = sourceContext.rules.agentRules[ruleId]; | ||
const mode = sourceContext.policy[ruleId]; | ||
const { name, value } = sinkContext; | ||
@@ -43,0 +43,0 @@ |
@@ -19,2 +19,3 @@ /* | ||
const agentLib = require('@contrast/agent-lib'); | ||
const { installChildComponentsSync } = require('@contrast/common'); | ||
@@ -26,10 +27,3 @@ module.exports = function(core) { | ||
const rules = instantiateRulesFromConfig( | ||
core.config.protect.rules, | ||
core.config.protect.disabled_rules, | ||
protect.agentLib, | ||
); | ||
protect.rules = rules; | ||
require('./policy')(core); | ||
require('./throw-security-exception')(core); | ||
@@ -49,6 +43,3 @@ require('./make-response-blocker')(core); | ||
protect.install = function() { | ||
protect.inputAnalysis.install(); | ||
protect.inputTracing.install(); | ||
protect.hardening.install(); | ||
protect.errorHandlers.install(); | ||
installChildComponentsSync(protect) | ||
}; | ||
@@ -77,36 +68,1 @@ | ||
} | ||
/** | ||
* This function instatiates the rules as defined in the configuration into | ||
* some structure. I'm in no way convinced or asserting that this is the right | ||
* structure but it does get a usable definition of rules in place. The final | ||
* structure will change based on what exactly TS sends as well as what the needs | ||
* of the code accessing the rules, exclusions, virtual-patches, etc. | ||
* | ||
* @param {Object} rules the rules object in the config.protect object. | ||
* @param {string[]} disabled array of disabled rules from config.protect | ||
* @param {Object} agentLib the agent-lib instance | ||
* @returns {Object} { agentLibRules, agentLibRulesMask, agentRules } | ||
*/ | ||
function instantiateRulesFromConfig(rules, disabled, agentLib) { | ||
const agentLibRules = {}; | ||
let agentLibRulesMask = 0; | ||
const agentRules = {}; | ||
for (const ruleId in rules) { | ||
if (disabled.indexOf(ruleId) >= 0 || rules[ruleId].mode === 'off') { | ||
continue; | ||
} | ||
// [matt] this is awkward. we should probably make each nosql-injection-x | ||
// rule separate in the config and only convert them to 'nosql-injection' | ||
// for reporting. | ||
if (agentLib.RuleType[ruleId]) { | ||
agentLibRules[ruleId] = rules[ruleId]; | ||
agentLibRulesMask = agentLibRulesMask | agentLib.RuleType[ruleId]; | ||
} else { | ||
agentRules[ruleId] = rules[ruleId]; | ||
} | ||
} | ||
return { agentLibRules, agentLibRulesMask, agentRules }; | ||
} |
@@ -116,3 +116,3 @@ /* | ||
const { rules: { agentLibRules, agentLibRulesMask: mask } } = sourceContext; | ||
const { policy: { rulesMask } } = sourceContext; | ||
@@ -123,5 +123,5 @@ inputAnalysis.handleVirtualPatches(sourceContext, { URLS: connectInputs.rawUrl, HEADERS: connectInputs.headers }); | ||
let block = undefined; | ||
if (mask !== 0) { | ||
const findings = agentLib.scoreRequestConnect(mask, connectInputs, preferWW); | ||
block = mergeFindings(agentLibRules, sourceContext.findings, findings); | ||
if (rulesMask !== 0) { | ||
const findings = agentLib.scoreRequestConnect(rulesMask, connectInputs, preferWW); | ||
block = mergeFindings(sourceContext, findings); | ||
} | ||
@@ -203,3 +203,3 @@ | ||
const { rules, rules: { agentLibRulesMask: mask } } = sourceContext; | ||
const { policy: { rulesMask } } = sourceContext; | ||
const resultsList = []; | ||
@@ -213,3 +213,3 @@ const { UrlParameter } = agentLib.InputType; | ||
} | ||
const items = agentLib.scoreAtom(mask, value, UrlParameter, preferWW); | ||
const items = agentLib.scoreAtom(rulesMask, value, UrlParameter, preferWW); | ||
if (!items) { | ||
@@ -243,3 +243,3 @@ return; | ||
const block = mergeFindings(rules.agentLibRules, sourceContext.findings, urlParamsFindings); | ||
const block = mergeFindings(sourceContext, urlParamsFindings); | ||
@@ -270,6 +270,6 @@ if (block) { | ||
const { rules, rules: { agentLibRulesMask: mask } } = sourceContext; | ||
const cookieFindings = agentLib.scoreRequestConnect(mask, { cookies: cookiesArr }, preferWW); | ||
const { policy: { rulesMask } } = sourceContext; | ||
const cookieFindings = agentLib.scoreRequestConnect(rulesMask, { cookies: cookiesArr }, preferWW); | ||
const block = mergeFindings(rules.agentLibRules, sourceContext.findings, cookieFindings); | ||
const block = mergeFindings(sourceContext, cookieFindings); | ||
@@ -406,7 +406,8 @@ if (block) { | ||
function commonObjectAnalyzer(sourceContext, object, inputTypes) { | ||
const { rules, rules: { agentLibRulesMask: mask } } = sourceContext; | ||
const { policy: { rulesMask } } = sourceContext; | ||
if (!rulesMask) return; | ||
// use inputTypes to set params... | ||
const { keyType, inputType } = inputTypes; | ||
const inputTypeStr = inputTypes === jsonInputTypes ? 'Json' : 'Parameter'; | ||
const { Where } = agentLib.MongoQueryType; | ||
const resultsList = []; | ||
@@ -430,3 +431,3 @@ | ||
let itemType; | ||
let mongoQueryType; | ||
let isMongoQueryType; | ||
// this is a bit awkward now because nosql-injection-mongo is not integrated | ||
@@ -439,4 +440,4 @@ // into the scoreAtom() function (or the check_input() function it uses). as | ||
itemType = keyType; | ||
if (mask & agentLib.RuleType['nosql-injection-mongo']) { | ||
mongoQueryType = agentLib.getMongoQueryType(value); | ||
if (rulesMask & agentLib.RuleType['nosql-injection-mongo']) { | ||
isMongoQueryType = agentLib.isMongoQueryType(value); | ||
} | ||
@@ -446,4 +447,4 @@ } else { | ||
} | ||
let items = agentLib.scoreAtom(mask, value, itemType, preferWW); | ||
if (!items && !mongoQueryType) { | ||
let items = agentLib.scoreAtom(rulesMask, value, itemType, preferWW); | ||
if (!items && !isMongoQueryType) { | ||
return; | ||
@@ -457,3 +458,3 @@ } | ||
// that additional information is kept as well. | ||
if (mongoQueryType) { | ||
if (isMongoQueryType) { | ||
const inputToCheck = getValueAtKey(object, path, value); | ||
@@ -463,9 +464,4 @@ // because scoreRequestConnect() returns the query type in the value, we | ||
// to match is stored as `inputToCheck`. | ||
const inputType = typeof inputToCheck; | ||
// query types up to Where, inclusive, accept either string or object values. Where and above accept only string values | ||
if (mongoQueryType <= Where || inputType === 'string') { | ||
// the query-type/input-type combination is valid. add a synthesized item. | ||
const item = { ruleId: 'nosql-injection-mongo', score: 10, mongoContext: { inputToCheck } }; | ||
items.push(item); | ||
} | ||
const item = { ruleId: 'nosql-injection-mongo', score: 10, mongoContext: { inputToCheck } }; | ||
items.push(item); | ||
} | ||
@@ -479,4 +475,3 @@ // make each item a complete Finding | ||
key: type === 'Key' ? value : path[path.length - 1], | ||
// mimic scoreRequestConnect() returning the query type as the value | ||
value: mongoQueryType || value, | ||
value, | ||
score: item.score, | ||
@@ -504,3 +499,3 @@ idsList: [], | ||
return mergeFindings(rules.agentLibRules, sourceContext.findings, findings); | ||
return mergeFindings(sourceContext, findings); | ||
} | ||
@@ -560,7 +555,9 @@ | ||
*/ | ||
function mergeFindings(rules, findings, newFindings) { | ||
function mergeFindings(sourceContext, newFindings) { | ||
const { findings, policy } = sourceContext; | ||
if (!newFindings.trackRequest) { | ||
return findings.securityException; | ||
} | ||
normalizeFindings(rules, newFindings); | ||
normalizeFindings(policy, newFindings); | ||
@@ -584,3 +581,3 @@ findings.trackRequest = findings.trackRequest || newFindings.trackRequest; | ||
// | ||
function normalizeFindings(rules, findings) { | ||
function normalizeFindings(policy, findings) { | ||
// now both augment the rules and check to see if any require blocking | ||
@@ -609,3 +606,3 @@ // at perimeter. | ||
// block. | ||
const { mode } = rules[r.ruleId]; | ||
const mode = policy[r.ruleId]; | ||
if (r.score >= 90 && BLOCKING_MODES.includes(mode)) { | ||
@@ -612,0 +609,0 @@ r.blocked = true; |
@@ -18,2 +18,4 @@ /* | ||
const { installChildComponentsSync } = require('@contrast/common'); | ||
module.exports = function(core) { | ||
@@ -39,5 +41,6 @@ const inputAnalysis = core.protect.inputAnalysis = {}; | ||
// framework specific instrumentation | ||
require('./install/fastify3')(core); | ||
require('./install/fastify')(core); | ||
require('./install/koa2')(core); | ||
require('./install/express4')(core); | ||
require('./install/hapi')(core); | ||
@@ -49,7 +52,3 @@ // virtual patches | ||
inputAnalysis.install = function() { | ||
Object.values(inputAnalysis) | ||
.filter((property) => property.install) | ||
.forEach((library) => { | ||
library.install(); | ||
}); | ||
installChildComponentsSync(inputAnalysis); | ||
}; | ||
@@ -56,0 +55,0 @@ |
@@ -36,4 +36,14 @@ /* | ||
if (fnName === 'bodyParser.text' && typeof req.body === 'string') { | ||
try { | ||
sourceContext.parsedBody = JSON.parse(req.body); | ||
} catch (err) { | ||
logger.error({ err }, 'Error parsing with bodyParser.text()'); | ||
origNext(); | ||
return; | ||
} | ||
} | ||
try { | ||
inputAnalysis.handleParsedBody(sourceContext, req.body); | ||
inputAnalysis.handleParsedBody(sourceContext, sourceContext.parsedBody); | ||
} catch (err) { | ||
@@ -40,0 +50,0 @@ if (isSecurityException(err)) { |
@@ -37,2 +37,3 @@ /* | ||
this.protect = core.protect; | ||
this.patcher = core.patcher; | ||
this.makeSourceContext = this.protect.makeSourceContext; | ||
@@ -55,2 +56,3 @@ this.maxBodySize = 16 * 1024 * 1024; | ||
this.hookHttps(); | ||
this.hookHttp2(); | ||
} | ||
@@ -67,3 +69,3 @@ | ||
this.logger.debug('hooking library: http'); | ||
this.depHooks.resolve({ name: 'http' }, this.hookServer.bind(this)); | ||
this.depHooks.resolve({ name: 'http' }, (http) => this.hookServerEmit.call(this, http, 'httpServer')); | ||
} | ||
@@ -76,32 +78,74 @@ | ||
this.logger.debug('hooking library: https'); | ||
this.depHooks.resolve({ name: 'https' }, this.hookServer.bind(this)); | ||
this.depHooks.resolve({ name: 'https' }, (https) => this.hookServerEmit.call(this, https, 'httpsServer')); | ||
} | ||
/** | ||
* Instruments the `Server` prototype from `http(s)`. This patches `emit` and | ||
* Sets hooks to instrument `http2 Servers`. | ||
*/ | ||
hookHttp2() { | ||
this.logger.debug('hooking library: http2'); | ||
// http2 library does not expose its Server class, so we need to hook the createServer function | ||
this.depHooks.resolve({ name: 'http2' }, (http2) => this.hookCreateServer.call(this, http2, 'http2Server')); | ||
this.depHooks.resolve({ name: 'http2' }, (http2) => this.hookCreateServer.call(this, http2, 'http2SecureServer', 'createSecureServer')); | ||
this.logger.debug('hooking library: spdy'); | ||
this.depHooks.resolve({ name: 'spdy' }, (spdy) => this.hookServerEmit.call(this, spdy, 'spdyServer')); | ||
} | ||
/** | ||
* Instruments the `Server` prototype from `http(s)` or spdy's http2 Server. This patches `emit` and | ||
* invokes the protect service to do analysis when appropriate. | ||
* | ||
* @param {Object} xport The http(s) module export | ||
*/ | ||
hookServer(xport) { | ||
hookServerEmit(serverSource, sourceName) { | ||
serverSource.Server.prototype = this.patcher.patch(serverSource.Server.prototype, 'emit', { | ||
name: `${sourceName}.Server.prototype.emit`, | ||
patchType: 'initiate-handling', | ||
around: this.emitAroundHook.bind(this) | ||
}); | ||
} | ||
/** | ||
* Instruments the `Http2Server` prototype which results from the http2.createServer/createSecureServer() call. | ||
* This also patches `emit` and | ||
* invokes the protect service to do analysis when appropriate. | ||
*/ | ||
hookCreateServer(serverSource, sourceName, constructorName = 'createServer') { | ||
const self = this; | ||
const { | ||
Server: { | ||
prototype: { emit } | ||
} | ||
} = xport; | ||
return this.patcher.patch(serverSource, constructorName, { | ||
name: sourceName, | ||
patchType: 'initiate-handling', | ||
post(data) { | ||
xport.Server.prototype.emit = function(...args) { | ||
const [type] = args; | ||
const { result: server } = data; | ||
const serverPrototype = server ? Object.getPrototypeOf(server) : null; | ||
if (type !== 'request') { | ||
return emit.call(this, ...args); | ||
if (!serverPrototype) { | ||
self.logger.error('Unable to patch server prototype, continue without instrumentation'); | ||
return; | ||
} | ||
self.patcher.patch(serverPrototype, 'emit', { | ||
name: `${sourceName}.Server.prototype.emit`, | ||
patchType: 'req-async-storage', | ||
around: self.emitAroundHook.bind(self) | ||
}); | ||
} | ||
}); | ||
} | ||
const context = { instance: this, method: emit, args }; | ||
self.initiateRequestHandling(context); | ||
/** | ||
* The around hook for `emit` that | ||
* invokes the protect service to do analysis when appropriate. | ||
*/ | ||
emitAroundHook(next, data) { | ||
const [type] = data.args; | ||
return !!this._events[type]; | ||
}; | ||
if (type !== 'request') { | ||
return next(); | ||
} | ||
const context = { instance: data.obj, method: next, args: data.args }; | ||
this.initiateRequestHandling(context); | ||
return !!data.obj._events[type]; | ||
} | ||
@@ -124,11 +168,2 @@ | ||
// URL exclusions should be applied here. there is no point in doing any additional | ||
// work if the url is excluded for a particular rule, i.e., that rule should be removed | ||
// from the list of rules for this request. and if all rules are excluded for this url | ||
// then none of the following needs to be done. | ||
if (this.protect.rules.agentLibRulesMask === 0) { | ||
this.logger.debug('no agent-lib rules are enabled, not checking request'); | ||
return; | ||
} | ||
let store; | ||
@@ -144,4 +179,6 @@ let block; | ||
// nothing can be done if async context is not available. | ||
if (!store) { | ||
this.logger.debug('cannot acquire store for initiateRequestHandling()'); | ||
setImmediate(() => method.call(instance, ...args)); | ||
return; | ||
@@ -178,2 +215,3 @@ } | ||
}; | ||
// only add queries if it's known that 'qs' or equivalent won't be used. | ||
@@ -184,8 +222,11 @@ /* c8 ignore next 3 */ | ||
} | ||
if (inputAnalysis.virtualPatchesEvaluators?.length) { | ||
store.protect.virtualPatchesEvaluators.push(...inputAnalysis.virtualPatchesEvaluators.map((e) => new Map(e))); | ||
} | ||
if (inputAnalysis.ipDenylist?.length) { | ||
block = inputAnalysis.handleIpDenylist(store.protect, inputAnalysis.ipDenylist); | ||
} | ||
if (inputAnalysis.ipAllowlist?.length) { | ||
@@ -192,0 +233,0 @@ const allowed = inputAnalysis.handleIpAllowlist(store.protect, inputAnalysis.ipAllowlist); |
@@ -19,3 +19,8 @@ /* | ||
const util = require('util'); | ||
const { BLOCKING_MODES, isString, simpleTraverse } = require('@contrast/common'); | ||
const { | ||
ProtectRuleMode: { OFF }, | ||
BLOCKING_MODES, | ||
isString, | ||
simpleTraverse | ||
} = require('@contrast/common'); | ||
@@ -28,3 +33,3 @@ module.exports = function(core) { | ||
const { mode } = sourceContext.rules.agentLibRules[ruleId]; | ||
const mode = sourceContext.policy[ruleId]; | ||
@@ -220,3 +225,3 @@ if (BLOCKING_MODES.includes(mode)) { | ||
function getResultsByRuleId(ruleId, context) { | ||
if (context.rules.agentLibRules[ruleId].mode === 'off') { | ||
if (context.policy[ruleId] === OFF) { | ||
return; | ||
@@ -223,0 +228,0 @@ } |
@@ -18,17 +18,11 @@ /* | ||
/** | ||
* INPUT TRACING is a STAGE of Protect. | ||
* The specification can be found here https://protect-spec.prod.dotnet.contsec.com/guide/input-tracing.html. | ||
* | ||
* To view other STAGES see https://protect-spec.prod.dotnet.contsec.com/guide/protect-types.html#protection-types | ||
* @param {object} core composed dependencies | ||
* @returns {object} | ||
*/ | ||
const { installChildComponentsSync } = require('@contrast/common'); | ||
module.exports = function(core) { | ||
const inputTracing = core.protect.inputTracing = {}; | ||
// load the interfaces that will be used by input tracing instrumentation | ||
// api | ||
require('./handlers')(core); | ||
// load the instrumentation installers | ||
// instrumentation | ||
require('./install/child-process')(core); | ||
@@ -42,13 +36,9 @@ require('./install/fs')(core); | ||
require('./install/http')(core); | ||
require('./install/vm')(core); | ||
require('./install/eval')(core); | ||
require('./install/function')(core); | ||
// TODO: NODE-2360 (oracledb) | ||
inputTracing.install = function() { | ||
inputTracing.cpInstrumentation.install(); | ||
inputTracing.fsInstrumentation.install(); | ||
inputTracing.mongodbInstrumentation.install(); | ||
inputTracing.mysqlInstrumentation.install(); | ||
inputTracing.postgresInstrumentation.install(); | ||
inputTracing.sequelizeInstrumentation.install(); | ||
inputTracing.sqlite3Instrumentation.install(); | ||
inputTracing.httpInstrumentation.install(); | ||
// TODO: NODE-2360 (2260?) | ||
installChildComponentsSync(inputTracing); | ||
}; | ||
@@ -55,0 +45,0 @@ |
@@ -32,3 +32,3 @@ /* | ||
function install() { | ||
if (!global.ContrastMethods.__contrastEval) { | ||
if (!global.ContrastMethods.eval) { | ||
logger.error('Cannot install `eval` instrumentation - Contrast method DNE'); | ||
@@ -38,4 +38,4 @@ return; | ||
patcher.patch(global.ContrastMethods, '__contrastEval', { | ||
name: 'global.ContrastMethods.__contrastEval', | ||
patcher.patch(global.ContrastMethods, 'eval', { | ||
name: 'global.ContrastMethods.eval', | ||
patchType, | ||
@@ -42,0 +42,0 @@ pre: ({ args, hooked, orig }) => { |
@@ -19,2 +19,3 @@ /* | ||
module.exports = function(core) { | ||
const { protect } = core; | ||
@@ -83,5 +84,4 @@ function makeSourceContext(req, res) { | ||
// this should be changed to capture only the rules applicable to this | ||
// particular request (if any route exclusions, etc.) | ||
rules: core.protect.rules, | ||
policy: protect.getPolicy(), | ||
exclusions: [], | ||
@@ -103,46 +103,2 @@ virtualPatchesEvaluators: [], | ||
}, | ||
/* | ||
findings: { | ||
trackRequest: true, | ||
resultsList: [ | ||
// Example 2 | ||
{ | ||
// return value from agent-lib | ||
value: 'kill -9 1', | ||
type: 'PARAMETER_VALUE', | ||
ruleId: 'cmd-injection', | ||
path: ['path', 'to', 'val'], | ||
// other data added during lifecycle | ||
// could we add these by mutating agent-lib return values? | ||
// What if there are multiple injections for the same value? The `details` value | ||
// could be an array in that case, or is this too complicated. | ||
blocked: false, | ||
details: [ | ||
{ | ||
context: { | ||
id: 'child_process.exec', | ||
get stack() {}, // lazy | ||
command: 'sudo kill -9 1', | ||
index: 5, | ||
} | ||
}, | ||
{ | ||
sinkId: 'child_process.exec', | ||
get stack() {}, // lazy | ||
command: 'sudo kill -9 1', | ||
index: 5, | ||
}] | ||
}, | ||
] | ||
} | ||
// (scoreAtom() returns only the ruleId and score because the caller supplied | ||
// the input and type; no key or path is known to scoreAtom(). code calling | ||
// scoreAtom() will need to augment the finding to match the above.) | ||
// | ||
// each finding is augmented with additional properties | ||
// - blocked: false // set to true if the finding causes the request to be blocked | ||
// - mappedId: ruleId // normalized ruleId, e.g., nosql-injection-mongo => nosql-injection | ||
// - | ||
// */ | ||
}; | ||
@@ -153,3 +109,3 @@ | ||
core.protect.makeSourceContext = makeSourceContext; | ||
return core.protect.makeSourceContext = makeSourceContext; | ||
}; |
@@ -20,2 +20,3 @@ /* | ||
BLOCKING_MODES, | ||
ProtectRuleMode: { OFF }, | ||
InputType, | ||
@@ -54,5 +55,5 @@ isString, | ||
const ruleId = 'cmd-injection-semantic-dangerous-paths'; | ||
const { mode } = sourceContext.rules.agentRules[ruleId]; | ||
const mode = sourceContext.policy[ruleId]; | ||
if (mode == 'off') return; | ||
if (mode == OFF) return; | ||
@@ -68,5 +69,5 @@ const result = agentLib.containsDangerousPath(sinkContext.value); | ||
const ruleId = 'cmd-injection-semantic-chained-commands'; | ||
const { mode } = sourceContext.rules.agentRules[ruleId]; | ||
const mode = sourceContext.policy[ruleId]; | ||
if (mode == 'off') return; | ||
if (mode == OFF) return; | ||
@@ -82,5 +83,5 @@ const indexOfChaining = agentLib.indexOfChaining(sinkContext.value); | ||
const ruleId = 'cmd-injection-command-backdoors'; | ||
const { mode } = sourceContext.rules.agentRules[ruleId]; | ||
const mode = sourceContext.policy[ruleId]; | ||
if (mode == 'off') return; | ||
if (mode == OFF) return; | ||
@@ -87,0 +88,0 @@ const finding = findBackdoorInjection(sourceContext, sinkContext.value); |
{ | ||
"name": "@contrast/protect", | ||
"version": "1.4.0", | ||
"version": "1.5.0", | ||
"description": "Contrast service providing framework-agnostic Protect support", | ||
@@ -22,6 +22,6 @@ "license": "SEE LICENSE IN LICENSE", | ||
"@babel/types": "^7.16.8", | ||
"@contrast/agent-lib": "^5.0.0", | ||
"@contrast/common": "1.1.0", | ||
"@contrast/core": "1.3.0", | ||
"@contrast/esm-hooks": "1.1.4", | ||
"@contrast/agent-lib": "^5.1.0", | ||
"@contrast/common": "1.1.1", | ||
"@contrast/core": "1.4.0", | ||
"@contrast/esm-hooks": "1.1.5", | ||
"@contrast/scopes": "1.1.1", | ||
@@ -28,0 +28,0 @@ "builtin-modules": "^3.2.0", |
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
163521
57
4149
+ Added@contrast/agentify@1.1.0(transitive)
+ Added@contrast/common@1.1.1(transitive)
+ Added@contrast/config@1.1.5(transitive)
+ Added@contrast/core@1.4.0(transitive)
+ Added@contrast/esm-hooks@1.1.5(transitive)
+ Added@contrast/reporter@1.3.0(transitive)
+ Added@contrast/rewriter@1.1.0(transitive)
- Removed@contrast/agentify@1.0.5(transitive)
- Removed@contrast/common@1.1.0(transitive)
- Removed@contrast/config@1.1.4(transitive)
- Removed@contrast/core@1.3.0(transitive)
- Removed@contrast/esm-hooks@1.1.4(transitive)
- Removed@contrast/reporter@1.2.0(transitive)
- Removed@contrast/rewriter@1.0.4(transitive)
Updated@contrast/agent-lib@^5.1.0
Updated@contrast/common@1.1.1
Updated@contrast/core@1.4.0
Updated@contrast/esm-hooks@1.1.5