@aligent/cdk-header-change-detection 1.6.1 → 1.7.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 +13 -0
- package/README.md +65 -0
- package/docs/diagram.jpg +0 -0
- package/index.js +1 -1
- package/lib/header-change-detection.js +1 -1
- package/package.json +14 -10
- package/lib/lambda/header-check.ts +0 -360
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# @aligent/cdk-header-change-detection
|
|
2
|
+
|
|
3
|
+
## 1.7.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1523](https://github.com/aligent/cdk-constructs/pull/1523) [`2550ceb`](https://github.com/aligent/cdk-constructs/commit/2550cebd411cf2cfd5b92deba17e18a5a3d3d012) Thanks [@TheOrangePuff](https://github.com/TheOrangePuff)! - Fixes changeset publishing to work correctly by switching from dist-based builds to in-place compilation within packages
|
|
8
|
+
|
|
9
|
+
## 1.7.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- [#1512](https://github.com/aligent/cdk-constructs/pull/1512) [`a9a6231`](https://github.com/aligent/cdk-constructs/commit/a9a62319e4528ac2d23f3af96e96cb2427f242f8) Thanks [@TheOrangePuff](https://github.com/TheOrangePuff)! - Adds changeset package to handle release management
|
package/README.md
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# Aligent Header Change Detection Service
|
|
2
|
+
|
|
3
|
+
## Overview
|
|
4
|
+
|
|
5
|
+
Creates a Lambda function that periodically scans security headers and sends the results to SNS.
|
|
6
|
+
|
|
7
|
+
### Diagram
|
|
8
|
+
|
|
9
|
+

|
|
10
|
+
|
|
11
|
+
This service aims to comply with PCI DSS to cover the requirements outlined by section 11.6.1.
|
|
12
|
+
|
|
13
|
+
**11.6.1**: A change- and tamper-detection mechanism is deployed as follows:
|
|
14
|
+
|
|
15
|
+
> - To alert personnel to unauthorized modification (including indicators of compromise, changes, additions, and deletions) to the security-impacting HTTP headers and the script contents of payment pages as received by the consumer browser.
|
|
16
|
+
> - The mechanism is configured to evaluate the received HTTP headers and payment pages.
|
|
17
|
+
> - The mechanism functions are performed as follows:
|
|
18
|
+
> - At least weekly
|
|
19
|
+
> OR
|
|
20
|
+
> - Periodically (at the frequency defined in the entity’s targeted risk analysis, which is performed according to all elements specified in Requirement 12.3.1)
|
|
21
|
+
|
|
22
|
+
## Default config
|
|
23
|
+
|
|
24
|
+
By default, the following headers are monitored:
|
|
25
|
+
|
|
26
|
+
- Content-Security-Policy
|
|
27
|
+
- Content-Security-Policy-Report-Only
|
|
28
|
+
- Reporting-Endpoints
|
|
29
|
+
- Strict-Transport-Security
|
|
30
|
+
- X-Frame-Options
|
|
31
|
+
- X-Content-Type-Options
|
|
32
|
+
- Cross-Origin-Opener-Policy
|
|
33
|
+
- Cross-Origin-Embedder-Policy
|
|
34
|
+
- Cross-Origin-Resource-Policy
|
|
35
|
+
- Referrer-Policy
|
|
36
|
+
- Permission-Policy
|
|
37
|
+
- Cache-Control
|
|
38
|
+
|
|
39
|
+
## Usage
|
|
40
|
+
|
|
41
|
+
To include this in your CDK stack, add the following:
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
// Import required packages
|
|
45
|
+
import { SnsTopic } from "aws-cdk-lib/aws-events-targets";
|
|
46
|
+
import { Topic } from "aws-cdk-lib/aws-sns";
|
|
47
|
+
import { HeaderChangeDetection } from "@aligent/cdk-header-change-detection";
|
|
48
|
+
|
|
49
|
+
// Create a new SNS topic
|
|
50
|
+
const topic = new Topic(this, "Topic");
|
|
51
|
+
const snsTopic = new SnsTopic(topic);
|
|
52
|
+
|
|
53
|
+
// Pass the required props
|
|
54
|
+
new HeaderChangeDetection(this, "HeaderChangeDetection", { snsTopic });
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Local development
|
|
58
|
+
|
|
59
|
+
[NPM link](https://docs.npmjs.com/cli/v7/commands/npm-link) can be used to develop the module locally.
|
|
60
|
+
|
|
61
|
+
1. Pull this repository locally
|
|
62
|
+
2. `cd` into this repository
|
|
63
|
+
3. run `npm link`
|
|
64
|
+
4. `cd` into the downstream repo (target project, etc) and run `npm link '@aligent/cdk-header-change-detection'`
|
|
65
|
+
The downstream repository should now include a symlink to this module. Allowing local changes to be tested before pushing. You may want to update the version notation of the package in the downstream repository's `package.json`.
|
package/docs/diagram.jpg
ADDED
|
Binary file
|
package/index.js
CHANGED
|
@@ -3,4 +3,4 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.HeaderChangeDetection = void 0;
|
|
4
4
|
const header_change_detection_1 = require("./lib/header-change-detection");
|
|
5
5
|
Object.defineProperty(exports, "HeaderChangeDetection", { enumerable: true, get: function () { return header_change_detection_1.HeaderChangeDetection; } });
|
|
6
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
6
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSwyRUFHdUM7QUFFOUIsc0dBSlAsK0NBQXFCLE9BSU8iLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQge1xuICBIZWFkZXJDaGFuZ2VEZXRlY3Rpb24sXG4gIEhlYWRlckNoYW5nZURldGVjdGlvblByb3BzLFxufSBmcm9tIFwiLi9saWIvaGVhZGVyLWNoYW5nZS1kZXRlY3Rpb25cIjtcblxuZXhwb3J0IHsgSGVhZGVyQ2hhbmdlRGV0ZWN0aW9uLCBIZWFkZXJDaGFuZ2VEZXRlY3Rpb25Qcm9wcyB9O1xuIl19
|
|
@@ -74,4 +74,4 @@ class HeaderChangeDetection extends constructs_1.Construct {
|
|
|
74
74
|
}
|
|
75
75
|
}
|
|
76
76
|
exports.HeaderChangeDetection = HeaderChangeDetection;
|
|
77
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
77
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGVhZGVyLWNoYW5nZS1kZXRlY3Rpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJoZWFkZXItY2hhbmdlLWRldGVjdGlvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSw2Q0FBb0Q7QUFDcEQsMkRBQTZFO0FBQzdFLHVEQUFtRTtBQUNuRSx1RUFBMEU7QUFDMUUsdURBQStFO0FBQy9FLDJDQUF1QztBQUN2QywrQkFBNEI7QUFDNUIsc0RBQStDO0FBb0QvQyxNQUFNLE9BQU8sR0FBRztJQUNkLElBQUk7SUFDSixJQUFJO0lBQ0osNERBQTREO0NBQzdELENBQUM7QUFFRixNQUFNLGNBQWMsR0FBRztJQUNyQix5QkFBeUI7SUFDekIscUNBQXFDO0lBQ3JDLHFCQUFxQjtJQUNyQiwyQkFBMkI7SUFDM0IsaUJBQWlCO0lBQ2pCLHdCQUF3QjtJQUN4Qiw0QkFBNEI7SUFDNUIsOEJBQThCO0lBQzlCLDhCQUE4QjtJQUM5QixpQkFBaUI7SUFDakIsbUJBQW1CO0lBQ25CLGVBQWU7Q0FDaEIsQ0FBQztBQUVGLE1BQWEscUJBQXNCLFNBQVEsc0JBQVM7SUFDbEQsWUFBWSxLQUFnQixFQUFFLEVBQVUsRUFBRSxLQUFpQzs7UUFDekUsS0FBSyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsQ0FBQztRQUVqQixNQUFNLE9BQU8sR0FBRyxLQUFLLENBQUMsZUFBZSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLGNBQWMsQ0FBQztRQUU1RCxPQUFPLENBQUMsSUFBSSxDQUNWLEdBQUcsQ0FBQyxDQUFBLE1BQUEsS0FBSyxDQUFDLGlCQUFpQiwwQ0FBRSxHQUFHLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxNQUFNLENBQUMsV0FBVyxFQUFFLENBQUMsS0FBSSxFQUFFLENBQUMsQ0FDeEUsQ0FBQztRQUVGLE1BQU0sS0FBSyxHQUFHLElBQUksb0JBQUssQ0FBQyxJQUFJLEVBQUUsT0FBTyxFQUFFO1lBQ3JDLFlBQVksRUFBRTtnQkFDWixJQUFJLEVBQUUsS0FBSztnQkFDWCxJQUFJLEVBQUUsNEJBQWEsQ0FBQyxNQUFNO2FBQzNCO1lBQ0QsV0FBVyxFQUFFLDBCQUFXLENBQUMsZUFBZTtTQUN6QyxDQUFDLENBQUM7UUFFSCxNQUFNLFFBQVEsR0FBRyxJQUFJLGlCQUFJLENBQUMsSUFBSSxFQUFFLFdBQVcsRUFBRTtZQUMzQyxRQUFRLEVBQUUsS0FBSyxDQUFDLFFBQVEsSUFBSSxxQkFBUSxDQUFDLElBQUksQ0FBQyxzQkFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQztZQUM1RCxHQUFHLEtBQUssQ0FBQyxTQUFTO1NBQ25CLENBQUMsQ0FBQztRQUVILE1BQU0sTUFBTSxHQUFHLElBQUkscUJBQVEsQ0FBQyxJQUFJLEVBQUUsYUFBYSxFQUFFO1lBQy9DLFlBQVksRUFBRSx5QkFBWSxDQUFDLE1BQU07WUFDakMsT0FBTyxFQUFFLG9CQUFPLENBQUMsV0FBVztZQUM1QixPQUFPLEVBQUUsc0JBQXNCO1lBQy9CLE9BQU8sRUFBRSxLQUFLLENBQUMsYUFBYSxJQUFJLHNCQUFRLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztZQUNwRCxJQUFJLEVBQUUsaUJBQUksQ0FBQyxTQUFTLENBQUMsSUFBQSxXQUFJLEVBQUMsU0FBUyxFQUFFLFFBQVEsQ0FBQyxFQUFFO2dCQUM5QyxRQUFRLEVBQUU7b0JBQ1IsT0FBTztvQkFDUCxLQUFLLEVBQUUseUJBQVcsQ0FBQyxZQUFZLENBQUMsU0FBUyxDQUFDO29CQUMxQyxLQUFLLEVBQUUsSUFBSSxxQkFBTyxDQUFDO3dCQUNqQixXQUFXLEVBQUUsQ0FBQyxJQUFBLFdBQUksRUFBQyxTQUFTLEVBQUUsd0JBQXdCLENBQUMsQ0FBQztxQkFDekQsQ0FBQztpQkFDSDthQUNGLENBQUM7WUFDRixXQUFXLEVBQUU7Z0JBQ1gsSUFBSSxFQUFFLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQztnQkFDMUIsT0FBTyxFQUFFLE9BQU8sQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDO2dCQUMxQixLQUFLLEVBQUUsS0FBSyxDQUFDLFNBQVM7Z0JBQ3RCLFNBQVMsRUFBRSxLQUFLLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxRQUFRO2dCQUN4QyxvQkFBb0IsRUFBRSxDQUFDLEtBQUssQ0FBQyxrQkFBa0IsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQzthQUN0RTtTQUNGLENBQUMsQ0FBQztRQUVILFFBQVEsQ0FBQyxTQUFTLENBQUMsSUFBSSxtQ0FBYyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUM7UUFFL0MsS0FBSyxDQUFDLGNBQWMsQ0FBQyxNQUFNLENBQUMsQ0FBQztRQUM3QixLQUFLLENBQUMsYUFBYSxDQUFDLE1BQU0sQ0FBQyxDQUFDO1FBQzVCLEtBQUssQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLFlBQVksQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUM1QyxDQUFDO0NBQ0Y7QUFwREQsc0RBb0RDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgRG9ja2VySW1hZ2UsIER1cmF0aW9uIH0gZnJvbSBcImF3cy1jZGstbGliXCI7XG5pbXBvcnQgeyBBdHRyaWJ1dGVUeXBlLCBCaWxsaW5nTW9kZSwgVGFibGUgfSBmcm9tIFwiYXdzLWNkay1saWIvYXdzLWR5bmFtb2RiXCI7XG5pbXBvcnQgeyBSdWxlLCBSdWxlUHJvcHMsIFNjaGVkdWxlIH0gZnJvbSBcImF3cy1jZGstbGliL2F3cy1ldmVudHNcIjtcbmltcG9ydCB7IExhbWJkYUZ1bmN0aW9uLCBTbnNUb3BpYyB9IGZyb20gXCJhd3MtY2RrLWxpYi9hd3MtZXZlbnRzLXRhcmdldHNcIjtcbmltcG9ydCB7IEFyY2hpdGVjdHVyZSwgQ29kZSwgRnVuY3Rpb24sIFJ1bnRpbWUgfSBmcm9tIFwiYXdzLWNkay1saWIvYXdzLWxhbWJkYVwiO1xuaW1wb3J0IHsgQ29uc3RydWN0IH0gZnJvbSBcImNvbnN0cnVjdHNcIjtcbmltcG9ydCB7IGpvaW4gfSBmcm9tIFwicGF0aFwiO1xuaW1wb3J0IHsgRXNidWlsZCB9IGZyb20gXCJAYWxpZ2VudC9jZGstZXNidWlsZFwiO1xuXG5leHBvcnQgaW50ZXJmYWNlIEhlYWRlckNoYW5nZURldGVjdGlvblByb3BzIHtcbiAgLyoqXG4gICAqIExpc3Qgb2YgVVJMcyB0byBtb25pdG9yIGZvciBoZWFkZXIgY2hhbmdlc1xuICAgKi9cbiAgdXJsczogc3RyaW5nW107XG5cbiAgLyoqXG4gICAqIE9wdGlvbmFsIGxpc3Qgb2YgYWRkaXRpb25hbCBoZWFkZXJzIHRvIG1vbml0b3JcbiAgICpcbiAgICogQGRlZmF1bHQgW11cbiAgICovXG4gIGFkZGl0aW9uYWxIZWFkZXJzPzogc3RyaW5nW107XG5cbiAgLyoqXG4gICAqIE9wdGlvbmFsbHkgZGlzYWJsZSBhbGwgdGhlIGRlZmF1bHQgaGVhZGVyc1xuICAgKlxuICAgKiBAZGVmYXVsdCBmYWxzZVxuICAgKi9cbiAgZGlzYWJsZURlZmF1bHRzPzogYm9vbGVhbjtcblxuICAvKipcbiAgICogU05TIFRvcGljIHRvIHNlbmQgY2hhbmdlIGRldGVjdGlvbiBub3RpZmljYXRpb25zIHRvXG4gICAqL1xuICBzbnNUb3BpYzogU25zVG9waWM7XG5cbiAgLyoqXG4gICAqIFRoZSBzY2hlZHVsZSBmb3IgcGVyZm9ybWluZyB0aGUgaGVhZGVyIGNoZWNrXG4gICAqXG4gICAqIEBkZWZhdWx0IFNjaGVkdWxlLnJhdGUoRHVyYXRpb24uaG91cnMoMSkpXG4gICAqL1xuICBzY2hlZHVsZT86IFNjaGVkdWxlO1xuXG4gIC8qKlxuICAgKiBPcHRpb25hbGx5IHBhc3MgYW55IHJ1bGUgcHJvcGVydGllc1xuICAgKi9cbiAgcnVsZVByb3BzPzogUGFydGlhbDxSdWxlUHJvcHM+O1xuXG4gIC8qKlxuICAgKiBPcHRpb25hbGx5IGFjY2VwdCBIVFRQIHN0YXR1cyBjb2RlcyBvdGhlciB0aGFuIDIwMFxuICAgKlxuICAgKiBAZGVmYXVsdCBbXCIyMDBcIl1cbiAgICovXG4gIGFjY2VwdGVkSHR0cFN0YXR1cz86IHN0cmluZ1tdO1xuXG4gIC8qKlxuICAgKiBGb3IgZXh0ZW5kZWQgTGFtYmRhIHRpbWVvdXQuIERlZmF1bHQ6IDEwIHNlY29uZHNcbiAgICovXG4gIGxhbWJkYVRpbWVvdXQ/OiBEdXJhdGlvbjtcbn1cblxuY29uc3QgY29tbWFuZCA9IFtcbiAgXCJzaFwiLFxuICBcIi1jXCIsXG4gICdlY2hvIFwiRG9ja2VyIGJ1aWxkIG5vdCBzdXBwb3J0ZWQuIFBsZWFzZSBpbnN0YWxsIGVzYnVpbGQuXCInLFxuXTtcblxuY29uc3QgZGVmYXVsdEhlYWRlcnMgPSBbXG4gIFwiY29udGVudC1zZWN1cml0eS1wb2xpY3lcIixcbiAgXCJjb250ZW50LXNlY3VyaXR5LXBvbGljeS1yZXBvcnQtb25seVwiLFxuICBcInJlcG9ydGluZy1lbmRwb2ludHNcIixcbiAgXCJzdHJpY3QtdHJhbnNwb3J0LXNlY3VyaXR5XCIsXG4gIFwieC1mcmFtZS1vcHRpb25zXCIsXG4gIFwieC1jb250ZW50LXR5cGUtb3B0aW9uc1wiLFxuICBcImNyb3NzLW9yaWdpbi1vcGVuZXItcG9saWN5XCIsXG4gIFwiY3Jvc3Mtb3JpZ2luLWVtYmVkZGVyLXBvbGljeVwiLFxuICBcImNyb3NzLW9yaWdpbi1yZXNvdXJjZS1wb2xpY3lcIixcbiAgXCJyZWZlcnJlci1wb2xpY3lcIixcbiAgXCJwZXJtaXNzaW9uLXBvbGljeVwiLFxuICBcImNhY2hlLWNvbnRyb2xcIixcbl07XG5cbmV4cG9ydCBjbGFzcyBIZWFkZXJDaGFuZ2VEZXRlY3Rpb24gZXh0ZW5kcyBDb25zdHJ1Y3Qge1xuICBjb25zdHJ1Y3RvcihzY29wZTogQ29uc3RydWN0LCBpZDogc3RyaW5nLCBwcm9wczogSGVhZGVyQ2hhbmdlRGV0ZWN0aW9uUHJvcHMpIHtcbiAgICBzdXBlcihzY29wZSwgaWQpO1xuXG4gICAgY29uc3QgaGVhZGVycyA9IHByb3BzLmRpc2FibGVEZWZhdWx0cyA/IFtdIDogZGVmYXVsdEhlYWRlcnM7XG5cbiAgICBoZWFkZXJzLnB1c2goXG4gICAgICAuLi4ocHJvcHMuYWRkaXRpb25hbEhlYWRlcnM/Lm1hcChoZWFkZXIgPT4gaGVhZGVyLnRvTG93ZXJDYXNlKCkpIHx8IFtdKVxuICAgICk7XG5cbiAgICBjb25zdCB0YWJsZSA9IG5ldyBUYWJsZSh0aGlzLCBcIlRhYmxlXCIsIHtcbiAgICAgIHBhcnRpdGlvbktleToge1xuICAgICAgICBuYW1lOiBcIlVybFwiLFxuICAgICAgICB0eXBlOiBBdHRyaWJ1dGVUeXBlLlNUUklORyxcbiAgICAgIH0sXG4gICAgICBiaWxsaW5nTW9kZTogQmlsbGluZ01vZGUuUEFZX1BFUl9SRVFVRVNULFxuICAgIH0pO1xuXG4gICAgY29uc3Qgc2NoZWR1bGUgPSBuZXcgUnVsZSh0aGlzLCBcIkV2ZW50UnVsZVwiLCB7XG4gICAgICBzY2hlZHVsZTogcHJvcHMuc2NoZWR1bGUgfHwgU2NoZWR1bGUucmF0ZShEdXJhdGlvbi5ob3VycygxKSksXG4gICAgICAuLi5wcm9wcy5ydWxlUHJvcHMsXG4gICAgfSk7XG5cbiAgICBjb25zdCBsYW1iZGEgPSBuZXcgRnVuY3Rpb24odGhpcywgXCJIZWFkZXJDaGVja1wiLCB7XG4gICAgICBhcmNoaXRlY3R1cmU6IEFyY2hpdGVjdHVyZS5YODZfNjQsXG4gICAgICBydW50aW1lOiBSdW50aW1lLk5PREVKU18yMl9YLFxuICAgICAgaGFuZGxlcjogXCJoZWFkZXItY2hlY2suaGFuZGxlclwiLFxuICAgICAgdGltZW91dDogcHJvcHMubGFtYmRhVGltZW91dCB8fCBEdXJhdGlvbi5zZWNvbmRzKDEwKSxcbiAgICAgIGNvZGU6IENvZGUuZnJvbUFzc2V0KGpvaW4oX19kaXJuYW1lLCBcImxhbWJkYVwiKSwge1xuICAgICAgICBidW5kbGluZzoge1xuICAgICAgICAgIGNvbW1hbmQsXG4gICAgICAgICAgaW1hZ2U6IERvY2tlckltYWdlLmZyb21SZWdpc3RyeShcImJ1c3lib3hcIiksXG4gICAgICAgICAgbG9jYWw6IG5ldyBFc2J1aWxkKHtcbiAgICAgICAgICAgIGVudHJ5UG9pbnRzOiBbam9pbihfX2Rpcm5hbWUsIFwibGFtYmRhL2hlYWRlci1jaGVjay50c1wiKV0sXG4gICAgICAgICAgfSksXG4gICAgICAgIH0sXG4gICAgICB9KSxcbiAgICAgIGVudmlyb25tZW50OiB7XG4gICAgICAgIFVSTFM6IHByb3BzLnVybHMuam9pbihcIixcIiksXG4gICAgICAgIEhFQURFUlM6IGhlYWRlcnMuam9pbihcIixcIiksXG4gICAgICAgIFRBQkxFOiB0YWJsZS50YWJsZU5hbWUsXG4gICAgICAgIFRPUElDX0FSTjogcHJvcHMuc25zVG9waWMudG9waWMudG9waWNBcm4sXG4gICAgICAgIEFDQ0VQVEVEX0hUVFBfU1RBVFVTOiAocHJvcHMuYWNjZXB0ZWRIdHRwU3RhdHVzIHx8IFtcIjIwMFwiXSkuam9pbihcIixcIiksXG4gICAgICB9LFxuICAgIH0pO1xuXG4gICAgc2NoZWR1bGUuYWRkVGFyZ2V0KG5ldyBMYW1iZGFGdW5jdGlvbihsYW1iZGEpKTtcblxuICAgIHRhYmxlLmdyYW50V3JpdGVEYXRhKGxhbWJkYSk7XG4gICAgdGFibGUuZ3JhbnRSZWFkRGF0YShsYW1iZGEpO1xuICAgIHByb3BzLnNuc1RvcGljLnRvcGljLmdyYW50UHVibGlzaChsYW1iZGEpO1xuICB9XG59XG4iXX0=
|
package/package.json
CHANGED
|
@@ -1,35 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aligent/cdk-header-change-detection",
|
|
3
|
+
"version": "1.7.1",
|
|
3
4
|
"main": "index.js",
|
|
4
5
|
"license": "MIT",
|
|
5
|
-
"homepage": "https://github.com/aligent/cdk-constructs/packages/header-change-detection
|
|
6
|
+
"homepage": "https://github.com/aligent/aws-cdk-constructs/tree/main/packages/header-change-detection#readme",
|
|
6
7
|
"repository": {
|
|
7
8
|
"type": "git",
|
|
8
|
-
"url": "https://github.com/aligent/aws-cdk-
|
|
9
|
+
"url": "git+https://github.com/aligent/aws-cdk-constructs.git"
|
|
10
|
+
},
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/aligent/aws-cdk-constructs/issues"
|
|
9
13
|
},
|
|
10
14
|
"types": "index.d.ts",
|
|
11
15
|
"scripts": {
|
|
12
|
-
"build": "tsc"
|
|
16
|
+
"build": "tsc",
|
|
17
|
+
"test": "npx nx test header-change-detection",
|
|
18
|
+
"lint": "npx nx lint header-change-detection"
|
|
13
19
|
},
|
|
14
20
|
"devDependencies": {
|
|
15
21
|
"@types/jest": "^29.5.10",
|
|
16
22
|
"@types/node": "^20.6.3",
|
|
17
|
-
"aws-cdk": "^2.
|
|
23
|
+
"aws-cdk": "^2.1019.1",
|
|
18
24
|
"jest": "^29.7.0",
|
|
19
25
|
"ts-jest": "^29.1.1",
|
|
20
26
|
"ts-node": "^10.9.1",
|
|
21
27
|
"typescript": "^5.3.2"
|
|
22
28
|
},
|
|
23
29
|
"dependencies": {
|
|
24
|
-
"@aws-sdk/client-dynamodb": "^3.
|
|
25
|
-
"@aws-sdk/client-sns": "3.
|
|
30
|
+
"@aws-sdk/client-dynamodb": "^3.830.0",
|
|
31
|
+
"@aws-sdk/client-sns": "3.830.0",
|
|
26
32
|
"axios": "^1.8.3",
|
|
27
33
|
"source-map-support": "^0.5.21"
|
|
28
34
|
},
|
|
29
35
|
"peerDependencies": {
|
|
30
36
|
"aws-cdk-lib": "^2.168.0",
|
|
31
37
|
"constructs": "^10.4.2"
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
"version": "1.6.1"
|
|
35
|
-
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -1,360 +0,0 @@
|
|
|
1
|
-
// We know the environment variables will exist so safe to ignore this
|
|
2
|
-
/* eslint-disable @typescript-eslint/no-non-null-assertion */
|
|
3
|
-
|
|
4
|
-
import axios from "axios";
|
|
5
|
-
import {
|
|
6
|
-
DynamoDBClient,
|
|
7
|
-
BatchGetItemCommand,
|
|
8
|
-
BatchGetItemCommandInput,
|
|
9
|
-
KeysAndAttributes,
|
|
10
|
-
UpdateItemCommandInput,
|
|
11
|
-
UpdateItemCommand,
|
|
12
|
-
AttributeValueUpdate,
|
|
13
|
-
UpdateItemCommandOutput,
|
|
14
|
-
} from "@aws-sdk/client-dynamodb";
|
|
15
|
-
import { PublishCommand, PublishInput, SNSClient } from "@aws-sdk/client-sns";
|
|
16
|
-
|
|
17
|
-
const URLS = process.env.URLS;
|
|
18
|
-
const HEADERS = process.env.HEADERS;
|
|
19
|
-
const TABLE = process.env.TABLE!;
|
|
20
|
-
|
|
21
|
-
const config = "";
|
|
22
|
-
const DB_CLIENT = new DynamoDBClient(config);
|
|
23
|
-
|
|
24
|
-
const securityHeaders = HEADERS?.split(",") || [];
|
|
25
|
-
|
|
26
|
-
// Accept status 200 default
|
|
27
|
-
const ACCEPTED_HTTP_STATUS = (process.env.ACCEPTED_HTTP_STATUS || "200")
|
|
28
|
-
.split(",")
|
|
29
|
-
.map(code => parseInt(code.trim(), 10));
|
|
30
|
-
|
|
31
|
-
console.log("ACCEPTED_HTTP_STATUS:", ACCEPTED_HTTP_STATUS);
|
|
32
|
-
|
|
33
|
-
type Headers = Map<string, string | undefined>;
|
|
34
|
-
|
|
35
|
-
// A map of URLs and their headers
|
|
36
|
-
type URLHeaders = Map<string, Headers>;
|
|
37
|
-
|
|
38
|
-
export const handler = async () => {
|
|
39
|
-
const urls = URLS?.split(",") || [];
|
|
40
|
-
|
|
41
|
-
// Fetch stored headers
|
|
42
|
-
const [storedUrlHeaders, currentUrlHeaders] = await Promise.all([
|
|
43
|
-
getStoredValues(urls),
|
|
44
|
-
fetchHeaders(urls),
|
|
45
|
-
]);
|
|
46
|
-
|
|
47
|
-
// Find any differences between the headers
|
|
48
|
-
const headerDifferences = new Map<string, Difference[]>();
|
|
49
|
-
let differencesDetected = false;
|
|
50
|
-
const dbUpdates = urls.map(url => {
|
|
51
|
-
const currentHeaders = currentUrlHeaders.get(url);
|
|
52
|
-
const storedHeaders =
|
|
53
|
-
storedUrlHeaders.get(url) || new Map<string, string | undefined>();
|
|
54
|
-
|
|
55
|
-
if (!currentHeaders)
|
|
56
|
-
throw new Error(`Could not get current headers for ${url}`);
|
|
57
|
-
|
|
58
|
-
// Check all headers that we care about
|
|
59
|
-
headerDifferences.set(
|
|
60
|
-
url,
|
|
61
|
-
compareHeaders(securityHeaders, storedHeaders, currentHeaders)
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
const headersToUpdate: Headers = new Map<string, string | undefined>();
|
|
65
|
-
headerDifferences.get(url)?.forEach(difference => {
|
|
66
|
-
headersToUpdate.set(difference.header, difference.currentValue);
|
|
67
|
-
differencesDetected = true;
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
return updateStoredValues(url, headersToUpdate);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
await Promise.all(dbUpdates);
|
|
74
|
-
|
|
75
|
-
if (differencesDetected)
|
|
76
|
-
await sendToSns(formatDifferences(headerDifferences));
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
/**
|
|
80
|
-
* Fetch security headers for the given urls
|
|
81
|
-
*
|
|
82
|
-
* @param urls list of urls to fetch headers from
|
|
83
|
-
*/
|
|
84
|
-
const fetchHeaders = async (urls: string[]): Promise<URLHeaders> => {
|
|
85
|
-
const currentUrlHeaders: URLHeaders = new Map<string, Headers>();
|
|
86
|
-
|
|
87
|
-
// Make an axios request for each url
|
|
88
|
-
await Promise.all(
|
|
89
|
-
urls.map(url =>
|
|
90
|
-
axios.get(url, { validateStatus: () => true }).then(response => {
|
|
91
|
-
if (!ACCEPTED_HTTP_STATUS.includes(response.status)) {
|
|
92
|
-
console.warn(
|
|
93
|
-
`Skipping ${url} — status ${response.status} not allowed`
|
|
94
|
-
);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const headers: Headers = new Map<string, string | undefined>();
|
|
99
|
-
|
|
100
|
-
Object.entries(response.headers).forEach(([headerName, value]) => {
|
|
101
|
-
if (securityHeaders?.includes(headerName))
|
|
102
|
-
headers.set(headerName, value as string);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
currentUrlHeaders.set(url, headers);
|
|
106
|
-
})
|
|
107
|
-
)
|
|
108
|
-
);
|
|
109
|
-
|
|
110
|
-
return currentUrlHeaders;
|
|
111
|
-
};
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Get values stored in DynamoDB table from a list of string keys.
|
|
115
|
-
* Assumes the call is made to the header change detection table.
|
|
116
|
-
* This table has a primary key of Url (string) with an unknown
|
|
117
|
-
* number of string fields.
|
|
118
|
-
*
|
|
119
|
-
* @param keys array of strings
|
|
120
|
-
*/
|
|
121
|
-
const getStoredValues = async (keys: string[]): Promise<URLHeaders> => {
|
|
122
|
-
if (keys.length === 0) {
|
|
123
|
-
console.log("No keys were passed");
|
|
124
|
-
return new Map<string, Headers>();
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Construct the command input
|
|
128
|
-
const primaryKeys = keys.map(url => {
|
|
129
|
-
return {
|
|
130
|
-
Url: {
|
|
131
|
-
S: url,
|
|
132
|
-
},
|
|
133
|
-
};
|
|
134
|
-
});
|
|
135
|
-
const requestItems = {
|
|
136
|
-
[TABLE]: {
|
|
137
|
-
Keys: primaryKeys,
|
|
138
|
-
},
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
return dynamoBatchRequest(requestItems);
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
/**
|
|
145
|
-
* Update stored headers for the given url.
|
|
146
|
-
* If the header no longer has a value, delete it. Otherwise update it.
|
|
147
|
-
*
|
|
148
|
-
* @param url the url to update - this is the primary key
|
|
149
|
-
* @param headers record of headers to update
|
|
150
|
-
*/
|
|
151
|
-
const updateStoredValues = async (
|
|
152
|
-
url: string,
|
|
153
|
-
headers: Headers
|
|
154
|
-
): Promise<UpdateItemCommandOutput | undefined> => {
|
|
155
|
-
// Convert headers to attribute value update attributes
|
|
156
|
-
const attributes: Record<string, AttributeValueUpdate> = {};
|
|
157
|
-
headers.forEach((value, headerName) => {
|
|
158
|
-
// If the value exists update it, otherwise remove it
|
|
159
|
-
if (value) {
|
|
160
|
-
attributes[headerName] = {
|
|
161
|
-
Value: {
|
|
162
|
-
S: Array.isArray(value) ? value.join("; ") : value,
|
|
163
|
-
},
|
|
164
|
-
Action: "PUT",
|
|
165
|
-
};
|
|
166
|
-
} else {
|
|
167
|
-
attributes[headerName] = {
|
|
168
|
-
Action: "DELETE",
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
if (Object.values(attributes).length === 0) {
|
|
174
|
-
console.log(`No attribute value changes for ${url}`);
|
|
175
|
-
return;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return dynamoUpdateRequest(url, attributes);
|
|
179
|
-
};
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
* Recursive function to get multiple items from a DynamoDB table
|
|
183
|
-
*
|
|
184
|
-
* @param requestItems
|
|
185
|
-
* @returns Promise<URLHeaders>
|
|
186
|
-
*/
|
|
187
|
-
const dynamoBatchRequest = async (
|
|
188
|
-
requestItems: Record<string, KeysAndAttributes> | undefined
|
|
189
|
-
): Promise<URLHeaders> => {
|
|
190
|
-
console.log(
|
|
191
|
-
`Starting batch request with items: ${JSON.stringify(requestItems)}`
|
|
192
|
-
);
|
|
193
|
-
|
|
194
|
-
// Validate that request items has values
|
|
195
|
-
if (Object.keys(requestItems || {})?.length === 0)
|
|
196
|
-
return new Map<string, Headers>();
|
|
197
|
-
|
|
198
|
-
const batchGetInput: BatchGetItemCommandInput = {
|
|
199
|
-
RequestItems: requestItems,
|
|
200
|
-
};
|
|
201
|
-
const batchGetCommand = new BatchGetItemCommand(batchGetInput);
|
|
202
|
-
|
|
203
|
-
// Fetch stored stored headers
|
|
204
|
-
const response = await DB_CLIENT.send(batchGetCommand);
|
|
205
|
-
const responses = response.Responses?.[TABLE];
|
|
206
|
-
|
|
207
|
-
if (!responses) return new Map<string, Headers>();
|
|
208
|
-
|
|
209
|
-
console.log(
|
|
210
|
-
`Got following data from dynamo table: ${JSON.stringify(responses)}`
|
|
211
|
-
);
|
|
212
|
-
|
|
213
|
-
const storedUrlHeaders: URLHeaders = new Map<string, Headers>();
|
|
214
|
-
Object.values(responses).forEach(headers => {
|
|
215
|
-
const urlHeaders: Headers = new Map<string, string | undefined>();
|
|
216
|
-
|
|
217
|
-
let url = "";
|
|
218
|
-
Object.entries(headers).forEach(([headerName, value]) => {
|
|
219
|
-
if (headerName === "Url") {
|
|
220
|
-
url = value.S!;
|
|
221
|
-
} else {
|
|
222
|
-
urlHeaders.set(headerName, value.S!);
|
|
223
|
-
}
|
|
224
|
-
});
|
|
225
|
-
storedUrlHeaders.set(url, urlHeaders);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
// Process any remaining keys
|
|
229
|
-
const nextUrlHeaders = await dynamoBatchRequest(response.UnprocessedKeys);
|
|
230
|
-
|
|
231
|
-
// Merge data into one object and return
|
|
232
|
-
return new Map<string, Headers>([...storedUrlHeaders, ...nextUrlHeaders]);
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
/**
|
|
236
|
-
* Send an update command to DynamoDB
|
|
237
|
-
*
|
|
238
|
-
* @param url the url to update - this is the primary key
|
|
239
|
-
* @param attributes Record<string, AttributeValueUpdate>
|
|
240
|
-
*/
|
|
241
|
-
const dynamoUpdateRequest = async (
|
|
242
|
-
url: string,
|
|
243
|
-
attributes: Record<string, AttributeValueUpdate>
|
|
244
|
-
): Promise<UpdateItemCommandOutput> => {
|
|
245
|
-
console.log(
|
|
246
|
-
`Updating ${url} in table with: ${JSON.stringify(Object.entries(attributes))}`
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
const updateItemInput: UpdateItemCommandInput = {
|
|
250
|
-
TableName: TABLE,
|
|
251
|
-
Key: {
|
|
252
|
-
Url: {
|
|
253
|
-
S: url,
|
|
254
|
-
},
|
|
255
|
-
},
|
|
256
|
-
AttributeUpdates: attributes,
|
|
257
|
-
};
|
|
258
|
-
const updateItemCommand = new UpdateItemCommand(updateItemInput);
|
|
259
|
-
|
|
260
|
-
return DB_CLIENT.send(updateItemCommand);
|
|
261
|
-
};
|
|
262
|
-
|
|
263
|
-
interface Difference {
|
|
264
|
-
header: string;
|
|
265
|
-
storedValue: string | undefined;
|
|
266
|
-
currentValue: string | undefined;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/**
|
|
270
|
-
* Compare values of two lists of headers. Return any headers that have differences
|
|
271
|
-
* along with their stored and current values.
|
|
272
|
-
*
|
|
273
|
-
* @param headers list of headers we want to compare
|
|
274
|
-
* @param stored list of headers that were last found on the site
|
|
275
|
-
* @param current list of headers currently on the site
|
|
276
|
-
* @returns
|
|
277
|
-
*/
|
|
278
|
-
const compareHeaders = (
|
|
279
|
-
headers: string[],
|
|
280
|
-
stored: Headers,
|
|
281
|
-
current: Headers
|
|
282
|
-
): Difference[] => {
|
|
283
|
-
const differences: Difference[] = [];
|
|
284
|
-
|
|
285
|
-
headers.forEach(header => {
|
|
286
|
-
const currentValue = current.get(header);
|
|
287
|
-
const storedValue = stored.get(header);
|
|
288
|
-
|
|
289
|
-
if (currentValue !== storedValue) {
|
|
290
|
-
differences.push({
|
|
291
|
-
header,
|
|
292
|
-
storedValue: storedValue,
|
|
293
|
-
currentValue: currentValue,
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
});
|
|
297
|
-
|
|
298
|
-
return differences;
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Format the differences so they can be easily read in an email.
|
|
303
|
-
*
|
|
304
|
-
* Outputs a string that looks like this:
|
|
305
|
-
*
|
|
306
|
-
* Headers differences found:
|
|
307
|
-
* == https://aligent.com.au/ ===
|
|
308
|
-
*
|
|
309
|
-
* Header: example-header-name
|
|
310
|
-
* Stored Value: No stored value
|
|
311
|
-
* Current Value: example-value
|
|
312
|
-
*
|
|
313
|
-
* === https://aligent.com.au/contact ===
|
|
314
|
-
*
|
|
315
|
-
* Header: example-header-name
|
|
316
|
-
* Stored Value: No stored value
|
|
317
|
-
* Current Value: example-value
|
|
318
|
-
*
|
|
319
|
-
* Header: example-header-name-2
|
|
320
|
-
* Stored Value: previous-value
|
|
321
|
-
* Current Value: new-example-value
|
|
322
|
-
*
|
|
323
|
-
* @param differences Map<string, Difference[]> where the key is the URL
|
|
324
|
-
*/
|
|
325
|
-
const formatDifferences = (differences: Map<string, Difference[]>): string => {
|
|
326
|
-
const message = Array.from(differences.keys()).reduce((text, url) => {
|
|
327
|
-
console.log(text, url);
|
|
328
|
-
// Skip the url if there are no differences
|
|
329
|
-
if (differences.get(url)?.length === 0) {
|
|
330
|
-
return text;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Format headers nicely
|
|
334
|
-
const headers = differences.get(url)?.reduce((headerText, header) => {
|
|
335
|
-
return (headerText += `\r\nHeader: ${header.header}\r\nStored Value: ${header.storedValue}\r\nCurrent Value: ${header.currentValue}\r\n`);
|
|
336
|
-
}, "");
|
|
337
|
-
|
|
338
|
-
return `${text}\r\n=== ${url} ===\r\n ${headers}`;
|
|
339
|
-
}, "");
|
|
340
|
-
|
|
341
|
-
return `Header differences found${message}`;
|
|
342
|
-
};
|
|
343
|
-
|
|
344
|
-
const TOPIC_ARN = process.env.TOPIC_ARN!;
|
|
345
|
-
const SNS_CLIENT = new SNSClient();
|
|
346
|
-
|
|
347
|
-
/**
|
|
348
|
-
* Send a message to the SNS topic
|
|
349
|
-
*
|
|
350
|
-
* @param message string to send to sns
|
|
351
|
-
*/
|
|
352
|
-
const sendToSns = async (message: string) => {
|
|
353
|
-
const publishInput: PublishInput = {
|
|
354
|
-
TopicArn: TOPIC_ARN,
|
|
355
|
-
Message: message,
|
|
356
|
-
Subject: "Security Header Change detected",
|
|
357
|
-
};
|
|
358
|
-
const publishCommand = new PublishCommand(publishInput);
|
|
359
|
-
await SNS_CLIENT.send(publishCommand);
|
|
360
|
-
};
|