@appland/scanner 1.62.2 → 1.65.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,32 @@
1
+ # [@appland/scanner-v1.65.0](https://github.com/applandinc/appmap-js/compare/@appland/scanner-v1.64.0...@appland/scanner-v1.65.0) (2022-08-08)
2
+
3
+
4
+ ### Bug Fixes
5
+
6
+ * Remove an inadvenant console log ([5c11fc7](https://github.com/applandinc/appmap-js/commit/5c11fc77650e105f169ca0bcc4045312578e8881))
7
+
8
+
9
+ ### Features
10
+
11
+ * Add unauthenticated-encryption to default rule set ([2e3cf92](https://github.com/applandinc/appmap-js/commit/2e3cf9298b3cfe99b489ab8b2894e913a305fdd0))
12
+ * Check for unauthenticated encryption ([d393951](https://github.com/applandinc/appmap-js/commit/d393951c73c4492f1e95b52a2580fde10b256ee4))
13
+
14
+ # [@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)
15
+
16
+
17
+ ### Features
18
+
19
+ * Command scope falls back on root events ([3823a1f](https://github.com/applandinc/appmap-js/commit/3823a1f686212db49b87f2995baa2103a4e007d1))
20
+
21
+ # [@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)
22
+
23
+
24
+ ### Features
25
+
26
+ * Include a partial stack in the finding hash ([7e82f8a](https://github.com/applandinc/appmap-js/commit/7e82f8a0b13a1d0927aad73be4ee126d2d4695dc))
27
+ * Populate hash_v2 on each finding ([04470b7](https://github.com/applandinc/appmap-js/commit/04470b7f11e764d79a22eb297d0e6882f6f89a3f))
28
+ * Summarize local report using hash_v2 ([ffbde39](https://github.com/applandinc/appmap-js/commit/ffbde393c17f1f1572eb7653bad796d90662b943))
29
+
1
30
  # [@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
31
 
3
32
 
@@ -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
  }
@@ -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'),
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = {
4
+ title: 'Unauthenticated encryption',
5
+ enumerateScope: true,
6
+ impactDomain: 'Security',
7
+ references: {
8
+ 'A02:2021': 'https://owasp.org/Top10/A02_2021-Cryptographic_Failures/',
9
+ },
10
+ labels: ['crypto.encrypt', 'crypto.set_auth_data'],
11
+ };
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ function matcher(event, appMapIndex) {
4
+ if (!event.receiver)
5
+ return;
6
+ const objectId = event.receiver.object_id;
7
+ const setAuthData = appMapIndex.appMap.events
8
+ .filter((evt) => { var _a; return ((_a = evt.receiver) === null || _a === void 0 ? void 0 : _a.object_id) === objectId; })
9
+ .find((evt) => evt.labels.has('crypto.set_auth_data'));
10
+ if (!setAuthData) {
11
+ return [
12
+ {
13
+ event,
14
+ message: 'Encryption is not authenticated',
15
+ },
16
+ ];
17
+ }
18
+ }
19
+ function rule() {
20
+ return {
21
+ matcher,
22
+ where: (e) => e.labels.has('crypto.encrypt'),
23
+ };
24
+ }
25
+ exports.default = rule;
@@ -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,
@@ -1,26 +1,27 @@
1
1
  checks:
2
- - rule: authzBeforeAuthn
3
- # - rule: circularDependency
4
- - rule: deserializationOfUntrustedData
5
- - rule: execOfUntrustedCommand
6
- - rule: http500
7
- # - rule: illegalPackageDependency
8
- # - rule: incompatibleHttpClientRequest
9
- # - rule: insecureCompare
10
- # - rule: jobNotCancelled
11
- - rule: logoutWithoutSessionReset
12
- # - rule: missingAuthentication
13
- - rule: missingContentType
14
- - rule: nPlusOneQuery
15
- # - rule: queryFromInvalidPackage
16
- # - rule: queryFromView
17
- # - rule: rpcWithoutCircuitBreaker
18
- # - rule: saveWithoutValidation
19
- - rule: secretInLog
20
- # - rule: slowFunctionCall
21
- # - rule: slowHttpServerRequest
22
- # - rule: slowQuery
23
- - rule: tooManyJoins
24
- - rule: tooManyUpdates
25
- # - rule: unbatchedMaterializedQuery
26
- - rule: updateInGetRequest
2
+ - rule: authz-before-authn
3
+ # - rule: circular-dependency
4
+ - rule: deserialization-of-untrusted-data
5
+ - rule: exec-of-untrusted-command
6
+ - rule: http-500
7
+ # - rule: illegal-package-dependency
8
+ # - rule: incompatible-http-client-request
9
+ # - rule: insecure-compare
10
+ # - rule: job-not-cancelled
11
+ - rule: logout-without-session-reset
12
+ # - rule: missing-authentication
13
+ - rule: missing-content-type
14
+ - rule: n-plus-one-query
15
+ # - rule: query-from-invalid-package
16
+ # - rule: query-from-view
17
+ # - rule: rpc-without-circuit-breaker
18
+ # - rule: save-without-validation
19
+ - rule: secret-in-log
20
+ # - rule: slow-function-call
21
+ # - rule: slow-httpServer-request
22
+ # - rule: slow-query
23
+ - rule: too-many-joins
24
+ - rule: too-many-updates
25
+ # - rule: unbatched-materialized-query
26
+ - rule: unauthenticated-encryption
27
+ - rule: update-in-get-request
@@ -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;
@@ -0,0 +1,7 @@
1
+ ---
2
+ name: crypto.encrypt
3
+ rules:
4
+ - unauthenticated-encryption
5
+ ---
6
+
7
+ A function that performs encryption.
@@ -0,0 +1,7 @@
1
+ ---
2
+ name: crypto.set_auth_data
3
+ rules:
4
+ - unauthenticated-encryption
5
+ ---
6
+
7
+ A function that sets authenticated data for an encryption operation.
@@ -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.
@@ -0,0 +1,42 @@
1
+ ---
2
+ rule: unauthenticated-encryption
3
+ name: Unauthenticated encryption
4
+ title: Unauthenticated encryption
5
+ references:
6
+ A02:2021: https://owasp.org/Top10/A02_2021-Cryptographic_Failures/
7
+ impactDomain: Security
8
+ labels:
9
+ - crypto.encrypt
10
+ - crypto.set_auth_data
11
+ ---
12
+
13
+ Ensures that encryption operations use authenticated encryption.
14
+
15
+ ### Rule logic
16
+
17
+ Finds all events labeled `crypto.encrypt`. For each of these events, there should be another event
18
+ in the same AppMap that has the same `receiver.object_id` and also has the label
19
+ `crypto.set_auth_data`.
20
+
21
+ ### Notes
22
+
23
+ [OWASP recommends against the use of unauthenticated encryption](https://owasp.org/Top10/A02_2021-Cryptographic_Failures/).
24
+
25
+ ### Resolution
26
+
27
+ Change the encryption code to use a current authenticated encryption algorithm. At the time of this
28
+ writing, an example is `aes-256-gcm`.
29
+
30
+ Examples:
31
+
32
+ - [`OpenSSL::Cipher#auth_data=` (Ruby)](https://ruby-doc.org/stdlib-3.1.1/libdoc/openssl/rdoc/OpenSSL/Cipher.html#method-i-auth_data-3D)
33
+
34
+ ### Options
35
+
36
+ None
37
+
38
+ ### Examples
39
+
40
+ ```yaml
41
+ - rule: unauthenticated-encryption
42
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@appland/scanner",
3
- "version": "1.62.2",
3
+ "version": "1.65.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
  }