@appland/scanner 1.62.1 → 1.64.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 CHANGED
@@ -1,3 +1,26 @@
1
+ # [@appland/scanner-v1.64.0](https://github.com/applandinc/appmap-js/compare/@appland/scanner-v1.63.0...@appland/scanner-v1.64.0) (2022-08-04)
2
+
3
+
4
+ ### Features
5
+
6
+ * Command scope falls back on root events ([3823a1f](https://github.com/applandinc/appmap-js/commit/3823a1f686212db49b87f2995baa2103a4e007d1))
7
+
8
+ # [@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)
9
+
10
+
11
+ ### Features
12
+
13
+ * Include a partial stack in the finding hash ([7e82f8a](https://github.com/applandinc/appmap-js/commit/7e82f8a0b13a1d0927aad73be4ee126d2d4695dc))
14
+ * Populate hash_v2 on each finding ([04470b7](https://github.com/applandinc/appmap-js/commit/04470b7f11e764d79a22eb297d0e6882f6f89a3f))
15
+ * Summarize local report using hash_v2 ([ffbde39](https://github.com/applandinc/appmap-js/commit/ffbde393c17f1f1572eb7653bad796d90662b943))
16
+
17
+ # [@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)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * Return proper result for job-not-cancelled ([f7ee5da](https://github.com/applandinc/appmap-js/commit/f7ee5da073849881c3c553f08fc2dd82bb8c7965))
23
+
1
24
  # [@appland/scanner-v1.62.1](https://github.com/applandinc/appmap-js/compare/@appland/scanner-v1.62.0...@appland/scanner-v1.62.1) (2022-07-13)
2
25
 
3
26
 
@@ -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;
package/built/check.js CHANGED
@@ -9,7 +9,7 @@ class Check {
9
9
  }
10
10
  this.id = rule.id;
11
11
  this.options = options || makeOptions();
12
- this.scope = rule.scope || 'root';
12
+ this.scope = rule.scope || 'command';
13
13
  this.includeScope = [];
14
14
  this.excludeScope = [];
15
15
  this.includeEvent = [];
@@ -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.hash)) {
14
- findingSummary.findingHashes.add(finding.hash);
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.hash]),
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.hash)).size} unique)`;
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);
@@ -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 = {
@@ -34,14 +35,7 @@ class RuleChecker {
34
35
  }
35
36
  check(appMapFile, appMapIndex, check, findings) {
36
37
  return __awaiter(this, void 0, void 0, function* () {
37
- const numScopesChecked = yield this.checkScope(appMapFile, appMapIndex, check, check.scope, findings);
38
- if (numScopesChecked === 0 && check.scope === 'command') {
39
- yield this.checkScope(appMapFile, appMapIndex, check, 'root', findings);
40
- }
41
- });
42
- }
43
- checkScope(appMapFile, appMapIndex, check, scope, findings) {
44
- return __awaiter(this, void 0, void 0, function* () {
38
+ const scope = check.scope;
45
39
  if ((0, util_1.verbose)()) {
46
40
  console.warn(`Checking AppMap ${appMapIndex.appMap.name} with scope ${scope}`);
47
41
  }
@@ -55,9 +49,7 @@ class RuleChecker {
55
49
  yield events[i];
56
50
  }
57
51
  };
58
- let numScopes = 0;
59
52
  for (const scope of scopeIterator.scopes(callEvents())) {
60
- numScopes += 1;
61
53
  if ((0, util_1.verbose)()) {
62
54
  console.warn(`Scope ${scope.scope}`);
63
55
  }
@@ -74,7 +66,6 @@ class RuleChecker {
74
66
  yield this.checkEvent(scope.scope, scope.scope, appMapFile, appMapIndex, checkInstance, findings);
75
67
  }
76
68
  }
77
- return numScopes;
78
69
  });
79
70
  }
80
71
  checkEvent(event, scope, appMapFile, appMapIndex, checkInstance, findings) {
@@ -109,23 +100,16 @@ class RuleChecker {
109
100
  findingEvent.codeObject.location,
110
101
  ...findingEvent.ancestors().map((ancestor) => ancestor.codeObject.location),
111
102
  ].filter(Boolean);
112
- const hash = (0, crypto_1.createHash)('sha256');
113
- hash.update(findingEvent.hash);
114
- hash.update(checkInstance.ruleId);
103
+ const hashV1 = new hashV1_1.default(checkInstance.ruleId, findingEvent,
104
+ // findingEvent gets passed here as a relatedEvent, and if you look at HashV1 it
105
+ // gets added to the hash again. That's how it worked in V1 so it's here for compatibility.
106
+ additionalEvents || []);
107
+ const hashV2 = new hashV2_1.default(checkInstance.ruleId, findingEvent, participatingEvents);
115
108
  const uniqueEvents = new Set();
116
109
  const relatedEvents = [];
117
- [findingEvent].concat((additionalEvents || []).map(eventUtil_1.cloneEvent)).forEach((event) => {
118
- if (uniqueEvents.has(event.id)) {
119
- return;
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) => {
110
+ [findingEvent, ...(additionalEvents || []), ...Object.values(participatingEvents)]
111
+ .map(eventUtil_1.cloneEvent)
112
+ .forEach((event) => {
129
113
  if (uniqueEvents.has(event.id)) {
130
114
  return;
131
115
  }
@@ -138,7 +122,8 @@ class RuleChecker {
138
122
  ruleId: checkInstance.ruleId,
139
123
  ruleTitle: checkInstance.title,
140
124
  event: (0, eventUtil_1.cloneEvent)(findingEvent),
141
- hash: hash.digest('hex'),
125
+ hash: hashV1.digest(),
126
+ hash_v2: hashV2.digest(),
142
127
  stack,
143
128
  scope: (0, eventUtil_1.cloneEvent)(scope),
144
129
  message: message || checkInstance.title,
@@ -31,6 +31,7 @@ function build() {
31
31
  {
32
32
  event: event.event,
33
33
  message: `${event.event} provides authorization, but the request is not authenticated`,
34
+ participatingEvents: { request: rootEvent },
34
35
  },
35
36
  ];
36
37
  }
@@ -18,11 +18,14 @@ function build() {
18
18
  const missing = creationEvents.length - cancellationEvents.length;
19
19
  if (missing === 0)
20
20
  return;
21
- return creationEvents.map((jobCreationEvent) => ({
22
- event: jobCreationEvent,
23
- message: `Job created by ${jobCreationEvent.codeObject.prettyName} was not cancelled when the enclosing transaction rolled back`,
24
- participatingEvents: { beginTransaction: event },
25
- }));
21
+ const result = {
22
+ event: event,
23
+ message: `${missing} jobs are scheduled but not cancelled in a rolled back transaction`,
24
+ // if there's a mismatch and there are cancellations we can't tell
25
+ // for sure which creations they match, so return everything
26
+ relatedEvents: [...creationEvents, ...cancellationEvents],
27
+ };
28
+ return [result];
26
29
  }
27
30
  return {
28
31
  matcher,
@@ -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;
@@ -28,6 +28,7 @@ function build() {
28
28
  {
29
29
  event: event.event,
30
30
  message: `${event.event} logs out the user, but the HTTP session is not cleared`,
31
+ participatingEvents: { request: rootEvent },
31
32
  },
32
33
  ];
33
34
  }
@@ -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(e) {
12
- return (0, openapi_1.rpcRequestForEvent)(e).responseContentType === undefined;
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: { commonAncestor: ancestor },
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 = { logEvent: event };
71
+ const participatingEvents = {};
72
72
  if (match.generatorEvent) {
73
73
  participatingEvents.generatorEvent = match.generatorEvent;
74
74
  }
@@ -101,6 +101,7 @@ exports.default = {
101
101
  id: 'secret-in-log',
102
102
  title: 'Secret in log',
103
103
  labels: [Secret, Log],
104
+ scope: 'root',
104
105
  impactDomain: 'Security',
105
106
  enumerateScope: true,
106
107
  references: {
@@ -30,7 +30,6 @@ function build(options) {
30
30
  exports.default = {
31
31
  id: 'slow-function-call',
32
32
  title: 'Slow function call',
33
- scope: 'root',
34
33
  impactDomain: 'Performance',
35
34
  enumerateScope: true,
36
35
  description: (0, parseRuleDescription_1.default)('slowFunctionCall'),
@@ -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
- return `Data update performed in ${httpServerRequest.route}: ${e.sqlQuery}`;
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,
@@ -21,15 +21,28 @@ const Command = 'command.perform';
21
21
  const Job = 'job.perform';
22
22
  class CommandScope extends scopeIterator_1.default {
23
23
  *scopes(events) {
24
+ let found = false;
25
+ const roots = [];
24
26
  for (const event of events) {
27
+ if (event.isCall() && !event.parent) {
28
+ roots.push(event);
29
+ }
25
30
  if (event.isCall() &&
26
31
  (event.codeObject.labels.has(Command) ||
27
32
  event.codeObject.labels.has(Job) ||
28
33
  event.httpServerRequest)) {
34
+ found = true;
29
35
  yield new ScopeImpl(event);
30
36
  this.advanceToReturnEvent(event, events);
31
37
  }
32
38
  }
39
+ // If no true command is found, yield all root events.
40
+ if (!found) {
41
+ for (let index = 0; index < roots.length; index++) {
42
+ const event = roots[index];
43
+ yield new ScopeImpl(event);
44
+ }
45
+ }
33
46
  }
34
47
  }
35
48
  exports.default = CommandScope;
@@ -8,6 +8,7 @@ impactDomain: Security
8
8
  labels:
9
9
  - secret
10
10
  - log
11
+ scope: root
11
12
  ---
12
13
 
13
14
  Identifies when a known or assumed secret is written to a log. Logs are often transported into other
@@ -3,7 +3,6 @@ rule: slow-function-call
3
3
  name: Slow function call
4
4
  title: Slow function call
5
5
  impactDomain: Performance
6
- scope: root
7
6
  ---
8
7
 
9
8
  Ensures that function elapsed time does not exceed a threshold.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appland/scanner",
3
- "version": "1.62.1",
3
+ "version": "1.64.0",
4
4
  "description": "",
5
5
  "bin": "built/cli.js",
6
6
  "files": [
@@ -61,7 +61,6 @@
61
61
  "@appland/sql-parser": "^1.5.0",
62
62
  "@types/cli-progress": "^3.9.2",
63
63
  "ajv": "^8.8.2",
64
- "ansi-escapes": "^5.0.0",
65
64
  "applicationinsights": "^2.1.4",
66
65
  "async": "^3.2.3",
67
66
  "chalk": "^4.1.2",
@@ -86,16 +85,20 @@
86
85
  },
87
86
  "pkg": {
88
87
  "targets": [
89
- "node14-linux-x64",
90
- "node14-win-x64",
91
- "node14-macos-x64"
88
+ "node16-linux-x64",
89
+ "node16-win-x64",
90
+ "node16-macos-x64",
91
+ "node16-macos-arm64"
92
92
  ],
93
93
  "scripts": [
94
- "built/scanner/*.js"
94
+ "built/scanner/*.js",
95
+ "built/rules/**/*.js"
95
96
  ],
96
97
  "assets": [
97
98
  "built/sampleConfig/*.yml",
98
- "built/**/*.json"
99
+ "built/**/*.json",
100
+ "package.json",
101
+ "doc/**/*.md"
99
102
  ],
100
103
  "outputPath": "dist"
101
104
  }