@appland/scanner 1.69.0 → 1.70.1
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 +28 -0
- package/built/cli/scan/command.js +1 -1
- package/built/cli/scan/scanner.js +2 -2
- package/built/cli/scan/singleScan.js +2 -1
- package/built/cli/scan.js +60 -25
- package/built/rules/jwtAlgorithmNone.js +47 -0
- package/built/rules/jwtUnverifiedSignature.js +84 -0
- package/doc/rules/jwt-algorithm-none.md +25 -0
- package/doc/rules/jwt-unverified-signature.md +24 -0
- package/package.json +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,31 @@
|
|
|
1
|
+
# [@appland/scanner-v1.70.1](https://github.com/applandinc/appmap-js/compare/@appland/scanner-v1.70.0...@appland/scanner-v1.70.1) (2022-09-05)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* Skip bad files when running scanner on a directory ([d6d1e4e](https://github.com/applandinc/appmap-js/commit/d6d1e4e4eeac40424802169414b170961dfccc25))
|
|
7
|
+
|
|
8
|
+
# [@appland/scanner-v1.70.0](https://github.com/applandinc/appmap-js/compare/@appland/scanner-v1.69.1...@appland/scanner-v1.70.0) (2022-08-31)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* Don't attempt to destructure an undefined value ([b46e358](https://github.com/applandinc/appmap-js/commit/b46e358cf0fd0cc56a7f465268f87f219ab13c55))
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Features
|
|
17
|
+
|
|
18
|
+
* Add scan for presence of JWT signature verification ([a2b382b](https://github.com/applandinc/appmap-js/commit/a2b382bd571cfbc0fcdfa389ad382536f85eb671))
|
|
19
|
+
* Add scanner for usage of JWT with the `none` algorithm ([025ac89](https://github.com/applandinc/appmap-js/commit/025ac89f0538d5b4bfed7f36e3d09788f2a38076))
|
|
20
|
+
|
|
21
|
+
# [@appland/scanner-v1.69.1](https://github.com/applandinc/appmap-js/compare/@appland/scanner-v1.69.0...@appland/scanner-v1.69.1) (2022-08-29)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
### Bug Fixes
|
|
25
|
+
|
|
26
|
+
* Add missing dependencies ([97a5d02](https://github.com/applandinc/appmap-js/commit/97a5d02ff161b52200430d2123d8d9ab62037220))
|
|
27
|
+
* Don't attempt to resolve a remote app ID if running in watch mode ([8f21ff1](https://github.com/applandinc/appmap-js/commit/8f21ff1a3bc86292f70a2cd1446f682e525869aa))
|
|
28
|
+
|
|
1
29
|
# [@appland/scanner-v1.69.0](https://github.com/applandinc/appmap-js/compare/@appland/scanner-v1.68.0...@appland/scanner-v1.69.0) (2022-08-23)
|
|
2
30
|
|
|
3
31
|
|
|
@@ -75,7 +75,7 @@ exports.default = {
|
|
|
75
75
|
if (appmapDir)
|
|
76
76
|
yield (0, validateFile_1.default)('directory', appmapDir);
|
|
77
77
|
let appId = appIdArg;
|
|
78
|
-
if (!reportAllFindings)
|
|
78
|
+
if (!watch && !reportAllFindings)
|
|
79
79
|
appId = yield (0, resolveAppId_1.default)(appIdArg, appmapDir);
|
|
80
80
|
if (watch) {
|
|
81
81
|
const watchAppMapDir = appmapDir;
|
|
@@ -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
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
const parseRuleDescription_1 = __importDefault(require("./lib/parseRuleDescription"));
|
|
7
|
+
function getHeader(jwt) {
|
|
8
|
+
try {
|
|
9
|
+
const [header] = jwt.split('.');
|
|
10
|
+
const decodedHeader = Buffer.from(header, 'base64').toString('utf-8');
|
|
11
|
+
return JSON.parse(decodedHeader);
|
|
12
|
+
}
|
|
13
|
+
catch (_a) {
|
|
14
|
+
// the JWT is malformed
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
class JwtAlgoritmNoneLogic {
|
|
19
|
+
matcher(event) {
|
|
20
|
+
if (!event.returnValue)
|
|
21
|
+
return;
|
|
22
|
+
const matches = new Array();
|
|
23
|
+
const { value: jwt } = event.returnValue;
|
|
24
|
+
const header = getHeader(jwt);
|
|
25
|
+
if ((header === null || header === void 0 ? void 0 : header.alg) === 'none') {
|
|
26
|
+
matches.push({ event, message: 'Encoded JWT using the `none` algorithm' });
|
|
27
|
+
}
|
|
28
|
+
return matches;
|
|
29
|
+
}
|
|
30
|
+
where(event) {
|
|
31
|
+
return event.labels.has('jwt.encode');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
class JwtAlgoritmNone {
|
|
35
|
+
constructor() {
|
|
36
|
+
this.id = 'jwt-algorithm-none';
|
|
37
|
+
this.title = "JWT 'none' algorithm";
|
|
38
|
+
this.impactDomain = 'Security';
|
|
39
|
+
this.enumerateScope = true;
|
|
40
|
+
this.description = (0, parseRuleDescription_1.default)('jwtAlgorithmNone');
|
|
41
|
+
this.url = 'https://appland.com/docs/analysis/rules-reference.html#jwt-algorithm-none';
|
|
42
|
+
}
|
|
43
|
+
build() {
|
|
44
|
+
return new JwtAlgoritmNoneLogic();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
exports.default = new JwtAlgoritmNone();
|
|
@@ -0,0 +1,84 @@
|
|
|
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.Labels = void 0;
|
|
7
|
+
const parseRuleDescription_1 = __importDefault(require("./lib/parseRuleDescription"));
|
|
8
|
+
const models_1 = require("@appland/models");
|
|
9
|
+
var Labels;
|
|
10
|
+
(function (Labels) {
|
|
11
|
+
Labels["SignatureVerify"] = "jwt.signature.verify";
|
|
12
|
+
Labels["JwtDecode"] = "jwt.decode";
|
|
13
|
+
})(Labels = exports.Labels || (exports.Labels = {}));
|
|
14
|
+
// Attempt to identify and return a JWT from an array of parameters
|
|
15
|
+
function findJwt(parameters) {
|
|
16
|
+
if (!parameters)
|
|
17
|
+
return;
|
|
18
|
+
for (const param of parameters) {
|
|
19
|
+
const tokens = param.value.split('.');
|
|
20
|
+
if (tokens.length !== 3)
|
|
21
|
+
return;
|
|
22
|
+
const [header, payload, signature] = tokens;
|
|
23
|
+
return { header, payload, signature };
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
// Check if `obj` matches the JWT by value or by reference (receiverId)
|
|
27
|
+
function matchJwt(obj, jwt, receiverId) {
|
|
28
|
+
const byValue = jwt !== undefined && obj.value.startsWith(`${jwt.header}.${jwt.payload}`);
|
|
29
|
+
const byReference = receiverId !== undefined && receiverId === obj.object_id;
|
|
30
|
+
return byValue || byReference;
|
|
31
|
+
}
|
|
32
|
+
class JwtUnverifiedSignatureLogic {
|
|
33
|
+
matcher(event) {
|
|
34
|
+
var _a, _b, _c;
|
|
35
|
+
if (event.labels.has(Labels.SignatureVerify)) {
|
|
36
|
+
// This method is marked both as decode and signature verify. It is compliant.
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
let verified = false;
|
|
40
|
+
let receiverId;
|
|
41
|
+
const jwt = findJwt(event.parameters);
|
|
42
|
+
const matches = new Array();
|
|
43
|
+
// Don't track the receiver if it's static. We'll find references of the decoded JWT passed by
|
|
44
|
+
// function parameter instead.
|
|
45
|
+
if (!event.isStatic) {
|
|
46
|
+
receiverId = (_a = event.receiver) === null || _a === void 0 ? void 0 : _a.object_id;
|
|
47
|
+
}
|
|
48
|
+
for (const { event: child } of new models_1.EventNavigator(event).following()) {
|
|
49
|
+
if (!child.labels.has(Labels.SignatureVerify)) {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const matchesReceiver = receiverId !== undefined && receiverId === ((_b = child.receiver) === null || _b === void 0 ? void 0 : _b.object_id);
|
|
53
|
+
const matchesParameter = (_c = child.parameters) === null || _c === void 0 ? void 0 : _c.find((param) => matchJwt(param, jwt, receiverId));
|
|
54
|
+
if (matchesReceiver || matchesParameter) {
|
|
55
|
+
verified = true;
|
|
56
|
+
break;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!verified) {
|
|
60
|
+
matches.push({
|
|
61
|
+
event,
|
|
62
|
+
message: 'JWT signature is not validated',
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
return matches;
|
|
66
|
+
}
|
|
67
|
+
where(event) {
|
|
68
|
+
return event.labels.has('jwt.decode');
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
class JwtUnverifiedSignature {
|
|
72
|
+
constructor() {
|
|
73
|
+
this.id = 'jwt-unverified-signature';
|
|
74
|
+
this.title = 'Unverified signature';
|
|
75
|
+
this.impactDomain = 'Security';
|
|
76
|
+
this.enumerateScope = true;
|
|
77
|
+
this.description = (0, parseRuleDescription_1.default)('jwtUnverifiedSignature');
|
|
78
|
+
this.url = 'https://appland.com/docs/analysis/rules-reference.html#jwt-unverified-signature';
|
|
79
|
+
}
|
|
80
|
+
build() {
|
|
81
|
+
return new JwtUnverifiedSignatureLogic();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
exports.default = new JwtUnverifiedSignature();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
---
|
|
2
|
+
rule: jwt-algorithm-none
|
|
3
|
+
name: Jwt algorithm none
|
|
4
|
+
title: JWT 'none' algorithm
|
|
5
|
+
impactDomain: Security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Finds usage of unsecured JWTs which use the `none` algorithm. When declaring this algorithm, there
|
|
9
|
+
is no signature contained within the token that may be cryptographically verified. As a result, the
|
|
10
|
+
data encoded within the token may be easily forged.
|
|
11
|
+
|
|
12
|
+
### Rule logic
|
|
13
|
+
|
|
14
|
+
Any function which encodes a new JWT will have its return value checked for presence of the `none`
|
|
15
|
+
algorithm within the token header.
|
|
16
|
+
|
|
17
|
+
### Options
|
|
18
|
+
|
|
19
|
+
None
|
|
20
|
+
|
|
21
|
+
### Examples
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
- rule: jwt-algorithm-none
|
|
25
|
+
```
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
---
|
|
2
|
+
rule: jwt-unverified-signature
|
|
3
|
+
name: Jwt unverified signature
|
|
4
|
+
title: Unverified signature
|
|
5
|
+
impactDomain: Security
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Finds cases where a JWT is decoded but the signature is never verified. Without proper signature
|
|
9
|
+
verification, the service will unknowingly accept arbitrary token payloads from any origin.
|
|
10
|
+
|
|
11
|
+
### Rule logic
|
|
12
|
+
|
|
13
|
+
Following a function call to decode a JWT, a subsequent function call to verify the token signature
|
|
14
|
+
is expected.
|
|
15
|
+
|
|
16
|
+
### Options
|
|
17
|
+
|
|
18
|
+
None
|
|
19
|
+
|
|
20
|
+
### Examples
|
|
21
|
+
|
|
22
|
+
```yaml
|
|
23
|
+
- rule: jwt-unverified-signature
|
|
24
|
+
```
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@appland/scanner",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.70.1",
|
|
4
4
|
"description": "",
|
|
5
5
|
"bin": "built/cli.js",
|
|
6
6
|
"files": [
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"prettier": "^2.3.2",
|
|
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",
|
|
@@ -56,19 +57,21 @@
|
|
|
56
57
|
},
|
|
57
58
|
"dependencies": {
|
|
58
59
|
"@appland/client": "^1.3.0",
|
|
59
|
-
"@appland/models": "^1.
|
|
60
|
+
"@appland/models": "^1.18.1",
|
|
60
61
|
"@appland/openapi": "1.0.2",
|
|
61
62
|
"@appland/sql-parser": "^1.5.0",
|
|
62
63
|
"@types/cli-progress": "^3.9.2",
|
|
63
64
|
"ajv": "^8.8.2",
|
|
64
65
|
"applicationinsights": "^2.1.4",
|
|
65
66
|
"async": "^3.2.3",
|
|
67
|
+
"boxen": "^5.0.1",
|
|
66
68
|
"chalk": "^4.1.2",
|
|
67
69
|
"chokidar": "applandinc/chokidar#fix/new-file-new-directory-race-on-linux",
|
|
68
70
|
"cli-progress": "^3.11.0",
|
|
69
71
|
"conf": "^10.0.2",
|
|
70
72
|
"form-data": "^4.0.0",
|
|
71
73
|
"glob": "7.2.3",
|
|
74
|
+
"inquirer": "^8.1.2",
|
|
72
75
|
"js-yaml": "^4.1.0",
|
|
73
76
|
"lru-cache": "^6.0.0",
|
|
74
77
|
"minimatch": "^3.0.4",
|