@appland/scanner 1.62.0 → 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 CHANGED
@@ -1,3 +1,26 @@
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
+
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)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * Return proper result for job-not-cancelled ([f7ee5da](https://github.com/applandinc/appmap-js/commit/f7ee5da073849881c3c553f08fc2dd82bb8c7965))
16
+
17
+ # [@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)
18
+
19
+
20
+ ### Bug Fixes
21
+
22
+ * More reliable detection of appmap changes by scanner watch ([b0cc14d](https://github.com/applandinc/appmap-js/commit/b0cc14d61b7e27248975c35022a8cd4da070337b))
23
+
1
24
  # [@appland/scanner-v1.62.0](https://github.com/applandinc/appmap-js/compare/@appland/scanner-v1.61.0...@appland/scanner-v1.62.0) (2022-07-12)
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;
@@ -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 = {
@@ -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 hash = (0, crypto_1.createHash)('sha256');
113
- hash.update(findingEvent.hash);
114
- hash.update(checkInstance.ruleId);
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].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) => {
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: hash.digest('hex'),
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,
@@ -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
  }
@@ -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,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appland/scanner",
3
- "version": "1.62.0",
3
+ "version": "1.63.0",
4
4
  "description": "",
5
5
  "bin": "built/cli.js",
6
6
  "files": [
@@ -65,11 +65,11 @@
65
65
  "applicationinsights": "^2.1.4",
66
66
  "async": "^3.2.3",
67
67
  "chalk": "^4.1.2",
68
- "chokidar": "^3.5.3",
68
+ "chokidar": "applandinc/chokidar#fix/new-file-new-directory-race-on-linux",
69
69
  "cli-progress": "^3.11.0",
70
70
  "conf": "^10.0.2",
71
71
  "form-data": "^4.0.0",
72
- "glob": "^7.2.0",
72
+ "glob": "7.2.3",
73
73
  "js-yaml": "^4.1.0",
74
74
  "lru-cache": "^6.0.0",
75
75
  "minimatch": "^3.0.4",