@appland/scanner 1.70.0 → 1.70.2
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 +567 -439
- package/built/cli/scan/scanner.js +2 -2
- package/built/cli/scan/singleScan.js +2 -1
- package/built/cli/scan.js +60 -25
- package/doc/architecture.md +31 -16
- package/doc/labels/deserialize.unsafe.md +1 -1
- package/doc/rules/deserialization-of-untrusted-data.md +20 -19
- package/doc/rules/n-plus-one-query.md +0 -1
- package/doc/rules/query-from-invalid-package.md +2 -2
- package/package.json +4 -2
|
@@ -35,10 +35,10 @@ class ScannerBase {
|
|
|
35
35
|
this.configuration = configuration;
|
|
36
36
|
this.files = files;
|
|
37
37
|
}
|
|
38
|
-
scan() {
|
|
38
|
+
scan(skipErrors = false) {
|
|
39
39
|
return __awaiter(this, void 0, void 0, function* () {
|
|
40
40
|
const checks = yield (0, configurationProvider_1.loadConfig)(this.configuration);
|
|
41
|
-
const { appMapMetadata, findings } = yield (0, scan_1.default)(this.files, checks);
|
|
41
|
+
const { appMapMetadata, findings } = yield (0, scan_1.default)(this.files, checks, skipErrors);
|
|
42
42
|
return new scanResults_1.ScanResults(this.configuration, appMapMetadata, findings, checks);
|
|
43
43
|
});
|
|
44
44
|
}
|
|
@@ -25,6 +25,7 @@ const validateFile_1 = __importDefault(require("../validateFile"));
|
|
|
25
25
|
function singleScan(options) {
|
|
26
26
|
return __awaiter(this, void 0, void 0, function* () {
|
|
27
27
|
const { appmapFile, appmapDir, configuration, reportAllFindings, appId, ide, reportFile } = options;
|
|
28
|
+
const skipErrors = appmapDir !== undefined;
|
|
28
29
|
const files = yield (0, util_1.collectAppMapFiles)(appmapFile, appmapDir);
|
|
29
30
|
yield Promise.all(files.map((file) => __awaiter(this, void 0, void 0, function* () { return (0, validateFile_1.default)('file', file); })));
|
|
30
31
|
const scanner = yield (0, scanner_1.default)(reportAllFindings, configuration, files).catch((error) => {
|
|
@@ -32,7 +33,7 @@ function singleScan(options) {
|
|
|
32
33
|
});
|
|
33
34
|
const startTime = Date.now();
|
|
34
35
|
const [rawScanResults, findingStatuses] = yield Promise.all([
|
|
35
|
-
scanner.scan(),
|
|
36
|
+
scanner.scan(skipErrors),
|
|
36
37
|
scanner.fetchFindingStatus(appId, appmapDir),
|
|
37
38
|
]);
|
|
38
39
|
// Always report the raw data
|
package/built/cli/scan.js
CHANGED
|
@@ -18,6 +18,7 @@ const promises_1 = require("fs/promises");
|
|
|
18
18
|
const models_1 = require("@appland/models");
|
|
19
19
|
const ruleChecker_1 = __importDefault(require("../ruleChecker"));
|
|
20
20
|
const appMapIndex_1 = __importDefault(require("../appMapIndex"));
|
|
21
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
21
22
|
function batch(items, size, process) {
|
|
22
23
|
return __awaiter(this, void 0, void 0, function* () {
|
|
23
24
|
const left = [...items];
|
|
@@ -25,7 +26,37 @@ function batch(items, size, process) {
|
|
|
25
26
|
yield Promise.all(left.splice(0, size).map(process));
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
|
-
|
|
29
|
+
class Progress {
|
|
30
|
+
constructor(numFiles, numChecks) {
|
|
31
|
+
this.numFiles = numFiles;
|
|
32
|
+
this.numChecks = numChecks;
|
|
33
|
+
this.checks = 0;
|
|
34
|
+
if (process.stdout.isTTY)
|
|
35
|
+
this.bar = new cli_progress_1.default.SingleBar({ format: `Scanning [{bar}] {percentage}% | {value}/{total}` }, cli_progress_1.default.Presets.shades_classic);
|
|
36
|
+
else {
|
|
37
|
+
this.start = this.check = this.file = this.stop = () => { };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
start() {
|
|
41
|
+
var _a;
|
|
42
|
+
(_a = this.bar) === null || _a === void 0 ? void 0 : _a.start(this.numFiles * this.numChecks, 0);
|
|
43
|
+
}
|
|
44
|
+
check() {
|
|
45
|
+
var _a;
|
|
46
|
+
this.checks += 1;
|
|
47
|
+
(_a = this.bar) === null || _a === void 0 ? void 0 : _a.increment();
|
|
48
|
+
}
|
|
49
|
+
file() {
|
|
50
|
+
var _a;
|
|
51
|
+
(_a = this.bar) === null || _a === void 0 ? void 0 : _a.increment(this.numChecks - this.checks);
|
|
52
|
+
this.checks = 0;
|
|
53
|
+
}
|
|
54
|
+
stop() {
|
|
55
|
+
var _a;
|
|
56
|
+
(_a = this.bar) === null || _a === void 0 ? void 0 : _a.stop();
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function scan(files, checks, skipErrors = true) {
|
|
29
60
|
return __awaiter(this, void 0, void 0, function* () {
|
|
30
61
|
// TODO: Improve this by respecting .gitignore, or similar.
|
|
31
62
|
// For now, this addresses the main problem of encountering appmap-js and its appmap.json files
|
|
@@ -34,33 +65,37 @@ function scan(files, checks) {
|
|
|
34
65
|
const checker = new ruleChecker_1.default();
|
|
35
66
|
const appMapMetadata = {};
|
|
36
67
|
const findings = [];
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
return {
|
|
42
|
-
increment: () => { },
|
|
43
|
-
start: () => { },
|
|
44
|
-
stop: () => { },
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
const progress = newProgress();
|
|
48
|
-
progress.start(files.length * checks.length, 0);
|
|
68
|
+
const progress = new Progress(files.length, checks.length);
|
|
69
|
+
let lastError = null;
|
|
70
|
+
let anySuccess = false;
|
|
49
71
|
yield batch(files, 2, (file) => __awaiter(this, void 0, void 0, function* () {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
yield
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
72
|
+
try {
|
|
73
|
+
console.log(`scanning ${file}`);
|
|
74
|
+
const appMapData = yield (0, promises_1.readFile)(file, 'utf8');
|
|
75
|
+
const appMap = (0, models_1.buildAppMap)(appMapData).normalize().build();
|
|
76
|
+
const appMapIndex = new appMapIndex_1.default(appMap);
|
|
77
|
+
appMapMetadata[file] = appMap.metadata;
|
|
78
|
+
yield Promise.all(checks.map((check) => __awaiter(this, void 0, void 0, function* () {
|
|
79
|
+
const matchCount = findings.length;
|
|
80
|
+
yield checker.check(file, appMapIndex, check, findings);
|
|
81
|
+
progress.check();
|
|
82
|
+
const newMatches = findings.slice(matchCount, findings.length);
|
|
83
|
+
newMatches.forEach((match) => (match.appMapFile = file));
|
|
84
|
+
})));
|
|
85
|
+
anySuccess = true;
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
(0, node_assert_1.default)(error instanceof Error);
|
|
89
|
+
lastError = new Error(`Error processing "${file}"`, { cause: error });
|
|
90
|
+
if (!skipErrors)
|
|
91
|
+
throw lastError;
|
|
92
|
+
console.warn(lastError);
|
|
93
|
+
}
|
|
94
|
+
progress.file();
|
|
62
95
|
}));
|
|
63
96
|
progress.stop();
|
|
97
|
+
if (!anySuccess && lastError)
|
|
98
|
+
throw lastError;
|
|
64
99
|
return { appMapMetadata, findings };
|
|
65
100
|
});
|
|
66
101
|
}
|
package/doc/architecture.md
CHANGED
|
@@ -1,48 +1,63 @@
|
|
|
1
1
|
## Scanner architecture
|
|
2
2
|
|
|
3
|
-
See [@appland/models source code](https://github.com/applandinc/appmap-js/tree/main/packages/models)
|
|
3
|
+
See [@appland/models source code](https://github.com/applandinc/appmap-js/tree/main/packages/models)
|
|
4
|
+
for the JS API to AppMap data.
|
|
4
5
|
|
|
5
6
|
## Assertions
|
|
6
7
|
|
|
7
|
-
An Assertion tests each configured AppMap event to see if it matches some condition. The test is
|
|
8
|
+
An Assertion tests each configured AppMap event to see if it matches some condition. The test is
|
|
9
|
+
applied by a `matcher` fnuction.
|
|
8
10
|
|
|
9
|
-
If there is a match, the assertion returns a Finding. A Finding contains the type of check, the
|
|
11
|
+
If there is a match, the assertion returns a Finding. A Finding contains the type of check, the
|
|
12
|
+
event, and a descriptive message. Supporting (related) events may also be reported.
|
|
10
13
|
|
|
11
14
|
## Scopes
|
|
12
15
|
|
|
13
|
-
Each Assertion declares a Scope. The Scope is the set of events that will be checked by an instance
|
|
14
|
-
|
|
16
|
+
Each Assertion declares a Scope. The Scope is the set of events that will be checked by an instance
|
|
17
|
+
of the Assertion object. An Assertion can use a narrower scope to help avoid giving false positives.
|
|
18
|
+
For example, consider an Assertion that looks for "too many SQL queries". The Assertion only wants
|
|
19
|
+
to count SQL queries within the Scope of a single command - not the entire AppMap.
|
|
15
20
|
|
|
16
21
|
Scope examples (roughly ordered from broadest to narrowest):
|
|
17
22
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
+
- `all` All events in the AppMap will be processed by the same Assertion instance.
|
|
24
|
+
- `root` A new Assertion instance is created for each root event.
|
|
25
|
+
- `command` A new Assertion instance is created for each HTTP server request, and for each event
|
|
26
|
+
that is not a descendant of an HTTP server request AND has the label `command` or `job`.
|
|
27
|
+
- `http_server_request` A new Assertion instance is created for each HTTP server request.
|
|
28
|
+
- `transaction` A new Assertion instance is created for each database transaction in the AppMap.
|
|
23
29
|
|
|
24
30
|
## Event filters
|
|
25
31
|
|
|
26
32
|
Assertions use Event filters to choose which events are processed by the `matcher` function.
|
|
27
33
|
|
|
28
|
-
Event filters include the `where`, `include` and `exclude` conditions. Events must match the `where`
|
|
34
|
+
Event filters include the `where`, `include` and `exclude` conditions. Events must match the `where`
|
|
35
|
+
and `include` conditions, and must not match the `exclude` condition. The `where` condition is built
|
|
36
|
+
into the Assertion. The `include` and `exclude` conditions are blank, and exist to be customized by
|
|
37
|
+
the user.
|
|
29
38
|
|
|
30
39
|
## Examples
|
|
31
40
|
|
|
32
41
|
### HTTP 500
|
|
33
42
|
|
|
34
|
-
`http-500` assertion is a simple example. It specifies the `http_server_request` scope - so that
|
|
43
|
+
`http-500` assertion is a simple example. It specifies the `http_server_request` scope - so that
|
|
44
|
+
each HTTP server request is processed by a separate Assertion.
|
|
35
45
|
|
|
36
|
-
The `where` condition filter out events that don't have an `http_server_response` - for example, if
|
|
46
|
+
The `where` condition filter out events that don't have an `http_server_response` - for example, if
|
|
47
|
+
the server process was hard-killed in the middle of processing.
|
|
37
48
|
|
|
38
49
|
The `matcher` function returns true if the HTTP status code is between 500 and 599.
|
|
39
50
|
|
|
40
51
|
### Insecure compare
|
|
41
52
|
|
|
42
|
-
`insecure-compare` operates on the `all` scope - it looks for insecure compare across the entire
|
|
53
|
+
`insecure-compare` operates on the `all` scope - it looks for insecure compare across the entire
|
|
54
|
+
AppMap.
|
|
43
55
|
|
|
44
|
-
The `where` clause selects events that are labeled `string.equals` or `secret`. The `secret` label
|
|
56
|
+
The `where` clause selects events that are labeled `string.equals` or `secret`. The `secret` label
|
|
57
|
+
is used to build a Set of all the secrets that are generated/returned by function events in the
|
|
58
|
+
AppMap. When a `string.equals` function is encountered, the assertion returns true if:
|
|
45
59
|
|
|
46
60
|
1. The function has a receiver value and one parameter.
|
|
47
61
|
2. Both the receiver value and the parameter value are not BCrypted-strings.
|
|
48
|
-
3. Both the receiver value and the parameter value are either (a) a known secret or (b) match a
|
|
62
|
+
3. Both the receiver value and the parameter value are either (a) a known secret or (b) match a
|
|
63
|
+
secret regexp
|
|
@@ -11,4 +11,4 @@ Indicates that a function does not guarantee safe deserialization.
|
|
|
11
11
|
- Ruby [YAML.unsafe_load](https://docs.ruby-lang.org/en/3.0/Psych.html#method-c-unsafe_load)
|
|
12
12
|
- Ruby [Marshal.load](https://docs.ruby-lang.org/en/3.0/Marshal.html#method-c-load)
|
|
13
13
|
- Java
|
|
14
|
-
[javax.jms.ObjectMessage#getObject](https://docs.oracle.com/javaee/6/api/javax/jms/ObjectMessage.html#getObject())
|
|
14
|
+
[javax.jms.ObjectMessage#getObject](<https://docs.oracle.com/javaee/6/api/javax/jms/ObjectMessage.html#getObject()>)
|
|
@@ -18,27 +18,27 @@ data comes from an untrusted source and hasn't passed through a sanitization mec
|
|
|
18
18
|
|
|
19
19
|
### Rule logic
|
|
20
20
|
|
|
21
|
-
Finds all events labeled `deserialize.unsafe` that receive tainted data (as
|
|
22
|
-
|
|
21
|
+
Finds all events labeled `deserialize.unsafe` that receive tainted data (as determined by object
|
|
22
|
+
identity or string value) as an input.
|
|
23
23
|
|
|
24
24
|
For each of these events; checks if all the inputs have been sanitized.
|
|
25
25
|
|
|
26
|
-
Data that has been passed to a function labeled `deserialize.sanitize` is
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
Data that has been passed to a function labeled `deserialize.sanitize` is assumed to be sanitized
|
|
27
|
+
from this point onwards. Such a function could either check the value is sanitized (note no
|
|
28
|
+
verification is currently done to ensure this result is checked) or return the transformed value
|
|
29
|
+
after any necessary sanitization.
|
|
30
30
|
|
|
31
|
-
Data passed to a function labeled `deserialized.safe` is considered in all
|
|
32
|
-
|
|
33
|
-
|
|
31
|
+
Data passed to a function labeled `deserialized.safe` is considered in all functions called by it
|
|
32
|
+
(down the callstack). Functions that first sanitize data and then use an unsafe deserialization
|
|
33
|
+
function should carry this label.
|
|
34
34
|
|
|
35
|
-
The set of tracked tainted data initially includes the HTTP message parameters
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
The set of tracked tainted data initially includes the HTTP message parameters and is expanded to
|
|
36
|
+
include any non-primitive (ie. longer than 5 characters) observed outputs of functions that consume
|
|
37
|
+
tainted data.
|
|
38
38
|
|
|
39
|
-
The reliability of this rule now depends on completeness of the AppMap.
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
The reliability of this rule now depends on completeness of the AppMap. If there is a data
|
|
40
|
+
transformation that is not captured it's invisible to the rule and will result in failure to
|
|
41
|
+
associate it with the tracked untrusted data.
|
|
42
42
|
|
|
43
43
|
### Notes
|
|
44
44
|
|
|
@@ -47,12 +47,13 @@ that executes code shortly after deserialization.
|
|
|
47
47
|
|
|
48
48
|
### Resolution
|
|
49
49
|
|
|
50
|
-
Consider if the library you're using offers a safe deserialization function variant that you can
|
|
51
|
-
|
|
50
|
+
Consider if the library you're using offers a safe deserialization function variant that you can use
|
|
51
|
+
instead. Using unsafe functions is only rarely needed and typically requires a good reason.
|
|
52
52
|
|
|
53
53
|
If you need to use the unsafe function, make sure you're able to handle unexpected input safely.
|
|
54
|
-
Sanitize the data thoroughly first; label the sanitization function with `deserialize.sanitize`
|
|
55
|
-
or wrap the whole sanitization and deserialization logic in a function labeled
|
|
54
|
+
Sanitize the data thoroughly first; label the sanitization function with `deserialize.sanitize`
|
|
55
|
+
label or wrap the whole sanitization and deserialization logic in a function labeled
|
|
56
|
+
`deserialize.safe`.
|
|
56
57
|
|
|
57
58
|
If you need to deserialize untrusted data, JSON is often a good choice as it is only capable of
|
|
58
59
|
returning ‘primitive’ types such as strings, arrays, hashes, numbers and nil. If you need to
|
|
@@ -29,8 +29,8 @@ the allowed packages.
|
|
|
29
29
|
|
|
30
30
|
- `allowedPackages: `[MatchPatternConfig](/docs/analysis/match-pattern-config.html)`[]`. Packages
|
|
31
31
|
which are allowed to make queries. Required.
|
|
32
|
-
- `allowedQueries: `[MatchPatternConfig](/docs/analysis/match-pattern-config.html)`[]`. Queries
|
|
33
|
-
are allowed from anywhere. Default:
|
|
32
|
+
- `allowedQueries: `[MatchPatternConfig](/docs/analysis/match-pattern-config.html)`[]`. Queries
|
|
33
|
+
which are allowed from anywhere. Default:
|
|
34
34
|
`[/\bBEGIN\b/i, /\bCOMMIT\b/i, /\bROLLBACK\b/i, /\bRELEASE\b/i, /\bSAVEPOINT\b/i]`.
|
|
35
35
|
|
|
36
36
|
### Examples
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appland/scanner",
|
|
3
|
-
"version": "1.70.
|
|
3
|
+
"version": "1.70.2",
|
|
4
4
|
"description": "",
|
|
5
5
|
"bin": "built/cli.js",
|
|
6
6
|
"files": [
|
|
@@ -46,9 +46,10 @@
|
|
|
46
46
|
"nock": "^13.2.2",
|
|
47
47
|
"openapi-types": "^9.3.0",
|
|
48
48
|
"pkg": "^5.5.2",
|
|
49
|
-
"prettier": "^2.
|
|
49
|
+
"prettier": "^2.7.1",
|
|
50
50
|
"semantic-release": "^19.0.2",
|
|
51
51
|
"sinon": "^13.0.1",
|
|
52
|
+
"tmp-promise": "^3.0.3",
|
|
52
53
|
"ts-jest": "^27.1.4",
|
|
53
54
|
"ts-json-schema-generator": "^0.97.0",
|
|
54
55
|
"ts-node": "^10.2.1",
|
|
@@ -76,6 +77,7 @@
|
|
|
76
77
|
"minimatch": "^3.0.4",
|
|
77
78
|
"octokat": "^0.10.0",
|
|
78
79
|
"openapi-diff": "^0.23.5",
|
|
80
|
+
"ora": "~5",
|
|
79
81
|
"pretty-format": "^27.4.6",
|
|
80
82
|
"read-pkg-up": "^7.0.1",
|
|
81
83
|
"supports-hyperlinks": "^2.2.0",
|