@appland/scanner 1.62.2 → 1.63.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/CHANGELOG.md +9 -0
- package/built/algorithms/hash/hashV1.js +29 -0
- package/built/algorithms/hash/hashV2.js +107 -0
- package/built/report/summaryReport.js +4 -4
- package/built/ruleChecker.js +12 -17
- package/built/rules/authzBeforeAuthn.js +1 -0
- package/built/rules/lib/isCommand.js +13 -0
- package/built/rules/logoutWithoutSessionReset.js +1 -0
- package/built/rules/missingContentType.js +4 -2
- package/built/rules/nPlusOneQuery.js +2 -1
- package/built/rules/secretInLog.js +1 -1
- package/built/rules/updateInGetRequest.js +9 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
# [@appland/scanner-v1.63.0](https://github.com/applandinc/appmap-js/compare/@appland/scanner-v1.62.2...@appland/scanner-v1.63.0) (2022-07-28)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* Include a partial stack in the finding hash ([7e82f8a](https://github.com/applandinc/appmap-js/commit/7e82f8a0b13a1d0927aad73be4ee126d2d4695dc))
|
|
7
|
+
* Populate hash_v2 on each finding ([04470b7](https://github.com/applandinc/appmap-js/commit/04470b7f11e764d79a22eb297d0e6882f6f89a3f))
|
|
8
|
+
* Summarize local report using hash_v2 ([ffbde39](https://github.com/applandinc/appmap-js/commit/ffbde393c17f1f1572eb7653bad796d90662b943))
|
|
9
|
+
|
|
1
10
|
# [@appland/scanner-v1.62.2](https://github.com/applandinc/appmap-js/compare/@appland/scanner-v1.62.1...@appland/scanner-v1.62.2) (2022-07-25)
|
|
2
11
|
|
|
3
12
|
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const crypto_1 = require("crypto");
|
|
4
|
+
class HashV1 {
|
|
5
|
+
constructor(ruleId, findingEvent, relatedEvents) {
|
|
6
|
+
this.hash = (0, crypto_1.createHash)('sha256');
|
|
7
|
+
this.hash.update(findingEvent.hash);
|
|
8
|
+
this.hash.update(ruleId);
|
|
9
|
+
// Admittedly odd, this implementation matches the original hash algorithm.
|
|
10
|
+
const uniqueEvents = new Set();
|
|
11
|
+
const hashEvents = [];
|
|
12
|
+
relatedEvents.unshift(findingEvent);
|
|
13
|
+
relatedEvents.forEach((event) => {
|
|
14
|
+
if (uniqueEvents.has(event.id))
|
|
15
|
+
return;
|
|
16
|
+
uniqueEvents.add(event.id);
|
|
17
|
+
hashEvents.push(event);
|
|
18
|
+
});
|
|
19
|
+
// This part where the hashes go into a Set, and there is some kind of ordering as a side-
|
|
20
|
+
// effect, is particularly weird.
|
|
21
|
+
new Set(hashEvents.map((e) => e.hash)).forEach((eventHash) => {
|
|
22
|
+
this.hash.update(eventHash);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
digest() {
|
|
26
|
+
return this.hash.digest('hex');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.default = HashV1;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.captureStack = void 0;
|
|
7
|
+
const crypto_1 = require("crypto");
|
|
8
|
+
const isCommand_1 = __importDefault(require("../../rules/lib/isCommand"));
|
|
9
|
+
const util_1 = require("../../rules/lib/util");
|
|
10
|
+
function hashEvent(entries, prefix, event) {
|
|
11
|
+
Object.keys(event.stableProperties)
|
|
12
|
+
.sort()
|
|
13
|
+
.forEach((key) => entries.push([[prefix, key].join('.'), event.stableProperties[key].toString()].join('=')));
|
|
14
|
+
}
|
|
15
|
+
const STACK_DEPTH = 3;
|
|
16
|
+
/**
|
|
17
|
+
* Captures stack entries from distinct packages. Ancestors of the event are traversed up to the
|
|
18
|
+
* command or root. Then, starting from the command or root, subsequent events which come from the
|
|
19
|
+
* same package as their preceding event are removed. Then the last N entries remaining in the
|
|
20
|
+
* stack are collected.
|
|
21
|
+
*
|
|
22
|
+
* @param event leaf event
|
|
23
|
+
* @param participatingEvents output collector
|
|
24
|
+
* @param depth number of events to include in the output
|
|
25
|
+
*/
|
|
26
|
+
function captureStack(event, depth = STACK_DEPTH) {
|
|
27
|
+
let ancestor = event.parent;
|
|
28
|
+
const stack = [];
|
|
29
|
+
while (ancestor) {
|
|
30
|
+
stack.push(ancestor);
|
|
31
|
+
ancestor = (0, isCommand_1.default)(ancestor) ? undefined : ancestor.parent;
|
|
32
|
+
}
|
|
33
|
+
const packageOf = (event) => {
|
|
34
|
+
if (!event)
|
|
35
|
+
return;
|
|
36
|
+
if (event.codeObject.type !== 'function')
|
|
37
|
+
return;
|
|
38
|
+
return event.codeObject.packageOf;
|
|
39
|
+
};
|
|
40
|
+
return stack
|
|
41
|
+
.filter((item, index) => item.codeObject.type !== 'function' || packageOf(stack[index + 1]) !== packageOf(item))
|
|
42
|
+
.slice(0, depth);
|
|
43
|
+
}
|
|
44
|
+
exports.captureStack = captureStack;
|
|
45
|
+
/**
|
|
46
|
+
* Builds a hash (digest) of a finding. The digest is constructed by first building a canonical
|
|
47
|
+
* string of the finding, of the form:
|
|
48
|
+
*
|
|
49
|
+
* ```
|
|
50
|
+
* [
|
|
51
|
+
* algorithmVersion=2
|
|
52
|
+
* rule=<rule-id>
|
|
53
|
+
* findingEvent.<property1>=value1
|
|
54
|
+
* ...
|
|
55
|
+
* findingEvent.<propertyN>=valueN
|
|
56
|
+
* participatingEvent.<eventName1>=value1
|
|
57
|
+
* ...
|
|
58
|
+
* participatingEvent.<eventName1>=valueN
|
|
59
|
+
* ...
|
|
60
|
+
* participatingEvent.<eventNameN>=value1
|
|
61
|
+
* ...
|
|
62
|
+
* participatingEvent.<eventNameN>=valueN
|
|
63
|
+
* stack[1]=value1
|
|
64
|
+
* ...
|
|
65
|
+
* stack[1]=valueN
|
|
66
|
+
* ...
|
|
67
|
+
* stack[3]=value1
|
|
68
|
+
* ...
|
|
69
|
+
* stack[3]=valueN
|
|
70
|
+
* ]
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* Participating events are sorted by the event name. Properties of each event are sorted by
|
|
74
|
+
* the property name. Event properties are provided by `Event#stableProperties`.
|
|
75
|
+
*
|
|
76
|
+
* The partial stack included in the finding hash removes subsequent function calls from the
|
|
77
|
+
* same package.
|
|
78
|
+
*/
|
|
79
|
+
class HashV2 {
|
|
80
|
+
constructor(ruleId, findingEvent, participatingEvents) {
|
|
81
|
+
this.hashEntries = [];
|
|
82
|
+
this.hash = (0, crypto_1.createHash)('sha256');
|
|
83
|
+
const hashEntries = [
|
|
84
|
+
['algorithmVersion', '2'],
|
|
85
|
+
['rule', ruleId],
|
|
86
|
+
].map((e) => e.join('='));
|
|
87
|
+
this.hashEntries = hashEntries;
|
|
88
|
+
hashEvent(hashEntries, 'findingEvent', findingEvent);
|
|
89
|
+
Object.keys(participatingEvents)
|
|
90
|
+
.sort()
|
|
91
|
+
.forEach((key) => {
|
|
92
|
+
const event = participatingEvents[key];
|
|
93
|
+
hashEvent(hashEntries, `participatingEvent.${key}`, event);
|
|
94
|
+
});
|
|
95
|
+
captureStack(findingEvent).forEach((event, index) => hashEvent(hashEntries, `stack[${index + 1}]`, event));
|
|
96
|
+
if ((0, util_1.verbose)())
|
|
97
|
+
console.log(hashEntries);
|
|
98
|
+
hashEntries.forEach((e) => this.hash.update(e));
|
|
99
|
+
}
|
|
100
|
+
get canonicalString() {
|
|
101
|
+
return this.hashEntries.join('\n');
|
|
102
|
+
}
|
|
103
|
+
digest() {
|
|
104
|
+
return this.hash.digest('hex');
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
exports.default = HashV2;
|
|
@@ -10,8 +10,8 @@ function summarizeFindings(findings) {
|
|
|
10
10
|
let findingSummary = memo[finding.ruleId];
|
|
11
11
|
if (findingSummary) {
|
|
12
12
|
findingSummary.findingTotal += 1;
|
|
13
|
-
if (!findingSummary.findingHashes.has(finding.
|
|
14
|
-
findingSummary.findingHashes.add(finding.
|
|
13
|
+
if (!findingSummary.findingHashes.has(finding.hash_v2)) {
|
|
14
|
+
findingSummary.findingHashes.add(finding.hash_v2);
|
|
15
15
|
findingSummary.messages.push(finding.message);
|
|
16
16
|
}
|
|
17
17
|
}
|
|
@@ -20,7 +20,7 @@ function summarizeFindings(findings) {
|
|
|
20
20
|
ruleId: finding.ruleId,
|
|
21
21
|
ruleTitle: finding.ruleTitle,
|
|
22
22
|
findingTotal: 1,
|
|
23
|
-
findingHashes: new Set([finding.
|
|
23
|
+
findingHashes: new Set([finding.hash_v2]),
|
|
24
24
|
messages: [finding.message],
|
|
25
25
|
};
|
|
26
26
|
memo[finding.ruleId] = findingSummary;
|
|
@@ -31,7 +31,7 @@ function summarizeFindings(findings) {
|
|
|
31
31
|
return Object.values(result);
|
|
32
32
|
}
|
|
33
33
|
function default_1(summary, colorize) {
|
|
34
|
-
const matchedStr = `${summary.summary.numFindings} ${(0, util_1.pluralize)('finding', summary.summary.numFindings)} (${new Set(summary.findings.map((finding) => finding.
|
|
34
|
+
const matchedStr = `${summary.summary.numFindings} ${(0, util_1.pluralize)('finding', summary.summary.numFindings)} (${new Set(summary.findings.map((finding) => finding.hash_v2)).size} unique)`;
|
|
35
35
|
const colouredMatchedStr = colorize ? chalk_1.default.stderr.magenta(matchedStr) : matchedStr;
|
|
36
36
|
console.log();
|
|
37
37
|
console.log(colouredMatchedStr);
|
package/built/ruleChecker.js
CHANGED
|
@@ -20,8 +20,9 @@ const httpClientRequestScope_1 = __importDefault(require("./scope/httpClientRequ
|
|
|
20
20
|
const commandScope_1 = __importDefault(require("./scope/commandScope"));
|
|
21
21
|
const sqlTransactionScope_1 = __importDefault(require("./scope/sqlTransactionScope"));
|
|
22
22
|
const checkInstance_1 = __importDefault(require("./checkInstance"));
|
|
23
|
-
const crypto_1 = require("crypto");
|
|
24
23
|
const eventUtil_1 = require("./eventUtil");
|
|
24
|
+
const hashV1_1 = __importDefault(require("./algorithms/hash/hashV1"));
|
|
25
|
+
const hashV2_1 = __importDefault(require("./algorithms/hash/hashV2"));
|
|
25
26
|
class RuleChecker {
|
|
26
27
|
constructor() {
|
|
27
28
|
this.scopes = {
|
|
@@ -109,23 +110,16 @@ class RuleChecker {
|
|
|
109
110
|
findingEvent.codeObject.location,
|
|
110
111
|
...findingEvent.ancestors().map((ancestor) => ancestor.codeObject.location),
|
|
111
112
|
].filter(Boolean);
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
hash.
|
|
113
|
+
const hashV1 = new hashV1_1.default(checkInstance.ruleId, findingEvent,
|
|
114
|
+
// findingEvent gets passed here as a relatedEvent, and if you look at HashV1 it
|
|
115
|
+
// gets added to the hash again. That's how it worked in V1 so it's here for compatibility.
|
|
116
|
+
additionalEvents || []);
|
|
117
|
+
const hashV2 = new hashV2_1.default(checkInstance.ruleId, findingEvent, participatingEvents);
|
|
115
118
|
const uniqueEvents = new Set();
|
|
116
119
|
const relatedEvents = [];
|
|
117
|
-
[findingEvent
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
uniqueEvents.add(event.id);
|
|
122
|
-
relatedEvents.push((0, eventUtil_1.cloneEvent)(event));
|
|
123
|
-
});
|
|
124
|
-
// Update event hash with unique hashes of related events
|
|
125
|
-
new Set(relatedEvents.map((e) => e.hash)).forEach((eventHash) => {
|
|
126
|
-
hash.update(eventHash);
|
|
127
|
-
});
|
|
128
|
-
Object.values(participatingEvents).forEach((event) => {
|
|
120
|
+
[findingEvent, ...(additionalEvents || []), ...Object.values(participatingEvents)]
|
|
121
|
+
.map(eventUtil_1.cloneEvent)
|
|
122
|
+
.forEach((event) => {
|
|
129
123
|
if (uniqueEvents.has(event.id)) {
|
|
130
124
|
return;
|
|
131
125
|
}
|
|
@@ -138,7 +132,8 @@ class RuleChecker {
|
|
|
138
132
|
ruleId: checkInstance.ruleId,
|
|
139
133
|
ruleTitle: checkInstance.title,
|
|
140
134
|
event: (0, eventUtil_1.cloneEvent)(findingEvent),
|
|
141
|
-
hash:
|
|
135
|
+
hash: hashV1.digest(),
|
|
136
|
+
hash_v2: hashV2.digest(),
|
|
142
137
|
stack,
|
|
143
138
|
scope: (0, eventUtil_1.cloneEvent)(scope),
|
|
144
139
|
message: message || checkInstance.title,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
function isCommand(event) {
|
|
4
|
+
let label;
|
|
5
|
+
if (event.labels.has('command'))
|
|
6
|
+
label = 'command';
|
|
7
|
+
else if (event.labels.has('job'))
|
|
8
|
+
label = 'job';
|
|
9
|
+
else if (event.httpServerRequest)
|
|
10
|
+
label = 'request';
|
|
11
|
+
return label;
|
|
12
|
+
}
|
|
13
|
+
exports.default = isCommand;
|
|
@@ -8,8 +8,10 @@ const parseRuleDescription_1 = __importDefault(require("./lib/parseRuleDescripti
|
|
|
8
8
|
const isRedirect = (status) => [301, 302, 303, 307, 308].includes(status);
|
|
9
9
|
const hasContent = (status) => status !== 204;
|
|
10
10
|
function build() {
|
|
11
|
-
function matcher(
|
|
12
|
-
|
|
11
|
+
function matcher(event) {
|
|
12
|
+
if ((0, openapi_1.rpcRequestForEvent)(event).responseContentType === undefined) {
|
|
13
|
+
return `Missing HTTP content type in response to request: ${event.route}`;
|
|
14
|
+
}
|
|
13
15
|
}
|
|
14
16
|
function where(e) {
|
|
15
17
|
return (!!e.httpServerResponse &&
|
|
@@ -35,6 +35,7 @@ function build(options) {
|
|
|
35
35
|
const ancestor = eventsById[parseInt(ancestorId)];
|
|
36
36
|
const occurranceCount = events.length;
|
|
37
37
|
if (occurranceCount > options.warningLimit) {
|
|
38
|
+
const participatingEvents = { commonAncestor: ancestor };
|
|
38
39
|
const buildMatchResult = (level) => {
|
|
39
40
|
return {
|
|
40
41
|
level: level,
|
|
@@ -43,7 +44,7 @@ function build(options) {
|
|
|
43
44
|
groupMessage: sql,
|
|
44
45
|
occurranceCount: occurranceCount,
|
|
45
46
|
relatedEvents: events.map((e) => e.event),
|
|
46
|
-
participatingEvents
|
|
47
|
+
participatingEvents,
|
|
47
48
|
};
|
|
48
49
|
};
|
|
49
50
|
if (occurranceCount >= options.errorLimit) {
|
|
@@ -68,7 +68,7 @@ const findInLog = (event) => {
|
|
|
68
68
|
if (matches.length > 0) {
|
|
69
69
|
return matches.map((match) => {
|
|
70
70
|
const { pattern, value } = match;
|
|
71
|
-
const participatingEvents = {
|
|
71
|
+
const participatingEvents = {};
|
|
72
72
|
if (match.generatorEvent) {
|
|
73
73
|
participatingEvents.generatorEvent = match.generatorEvent;
|
|
74
74
|
}
|
|
@@ -5,6 +5,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
const util_1 = require("./lib/util");
|
|
7
7
|
const parseRuleDescription_1 = __importDefault(require("./lib/parseRuleDescription"));
|
|
8
|
+
const assert_1 = __importDefault(require("assert"));
|
|
8
9
|
class Options {
|
|
9
10
|
constructor(queryInclude = [/\binsert\b/i, /\bupdate\b/i], queryExclude = []) {
|
|
10
11
|
this._queryInclude = queryInclude;
|
|
@@ -38,7 +39,14 @@ function build(options = new Options()) {
|
|
|
38
39
|
!options.queryExclude.some((pattern) => e.sqlQuery.match(pattern)) &&
|
|
39
40
|
!e.ancestors().some((ancestor) => ancestor.codeObject.labels.has(Audit)) &&
|
|
40
41
|
hasHttpServerRequest()) {
|
|
41
|
-
|
|
42
|
+
(0, assert_1.default)(httpServerRequest, 'HTTP server request is undefined');
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
event: e,
|
|
46
|
+
message: `Data update performed in HTTP request ${httpServerRequest.route}: ${e.sqlQuery}`,
|
|
47
|
+
participatingEvents: { request: httpServerRequest },
|
|
48
|
+
},
|
|
49
|
+
];
|
|
42
50
|
}
|
|
43
51
|
},
|
|
44
52
|
where: (e) => !!e.sqlQuery,
|