@aligent/cdk-header-change-detection 1.7.0 → 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 +6 -0
- package/index.d.ts +2 -0
- package/index.js +6 -0
- package/lib/header-change-detection.d.ts +49 -0
- package/lib/header-change-detection.js +77 -0
- package/package.json +1 -1
- package/index.ts +0 -6
- package/jest.config.ts +0 -11
- package/lib/header-change-detection.ts +0 -133
- package/lib/lambda/header-check.ts +0 -360
- package/project.json +0 -45
- package/tsconfig.app.json +0 -10
- package/tsconfig.json +0 -16
- package/tsconfig.spec.json +0 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# @aligent/cdk-header-change-detection
|
|
2
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
|
+
|
|
3
9
|
## 1.7.0
|
|
4
10
|
|
|
5
11
|
### Minor Changes
|
package/index.d.ts
ADDED
package/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HeaderChangeDetection = void 0;
|
|
4
|
+
const header_change_detection_1 = require("./lib/header-change-detection");
|
|
5
|
+
Object.defineProperty(exports, "HeaderChangeDetection", { enumerable: true, get: function () { return header_change_detection_1.HeaderChangeDetection; } });
|
|
6
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJpbmRleC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSwyRUFHdUM7QUFFOUIsc0dBSlAsK0NBQXFCLE9BSU8iLCJzb3VyY2VzQ29udGVudCI6WyJpbXBvcnQge1xuICBIZWFkZXJDaGFuZ2VEZXRlY3Rpb24sXG4gIEhlYWRlckNoYW5nZURldGVjdGlvblByb3BzLFxufSBmcm9tIFwiLi9saWIvaGVhZGVyLWNoYW5nZS1kZXRlY3Rpb25cIjtcblxuZXhwb3J0IHsgSGVhZGVyQ2hhbmdlRGV0ZWN0aW9uLCBIZWFkZXJDaGFuZ2VEZXRlY3Rpb25Qcm9wcyB9O1xuIl19
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Duration } from "aws-cdk-lib";
|
|
2
|
+
import { RuleProps, Schedule } from "aws-cdk-lib/aws-events";
|
|
3
|
+
import { SnsTopic } from "aws-cdk-lib/aws-events-targets";
|
|
4
|
+
import { Construct } from "constructs";
|
|
5
|
+
export interface HeaderChangeDetectionProps {
|
|
6
|
+
/**
|
|
7
|
+
* List of URLs to monitor for header changes
|
|
8
|
+
*/
|
|
9
|
+
urls: string[];
|
|
10
|
+
/**
|
|
11
|
+
* Optional list of additional headers to monitor
|
|
12
|
+
*
|
|
13
|
+
* @default []
|
|
14
|
+
*/
|
|
15
|
+
additionalHeaders?: string[];
|
|
16
|
+
/**
|
|
17
|
+
* Optionally disable all the default headers
|
|
18
|
+
*
|
|
19
|
+
* @default false
|
|
20
|
+
*/
|
|
21
|
+
disableDefaults?: boolean;
|
|
22
|
+
/**
|
|
23
|
+
* SNS Topic to send change detection notifications to
|
|
24
|
+
*/
|
|
25
|
+
snsTopic: SnsTopic;
|
|
26
|
+
/**
|
|
27
|
+
* The schedule for performing the header check
|
|
28
|
+
*
|
|
29
|
+
* @default Schedule.rate(Duration.hours(1))
|
|
30
|
+
*/
|
|
31
|
+
schedule?: Schedule;
|
|
32
|
+
/**
|
|
33
|
+
* Optionally pass any rule properties
|
|
34
|
+
*/
|
|
35
|
+
ruleProps?: Partial<RuleProps>;
|
|
36
|
+
/**
|
|
37
|
+
* Optionally accept HTTP status codes other than 200
|
|
38
|
+
*
|
|
39
|
+
* @default ["200"]
|
|
40
|
+
*/
|
|
41
|
+
acceptedHttpStatus?: string[];
|
|
42
|
+
/**
|
|
43
|
+
* For extended Lambda timeout. Default: 10 seconds
|
|
44
|
+
*/
|
|
45
|
+
lambdaTimeout?: Duration;
|
|
46
|
+
}
|
|
47
|
+
export declare class HeaderChangeDetection extends Construct {
|
|
48
|
+
constructor(scope: Construct, id: string, props: HeaderChangeDetectionProps);
|
|
49
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HeaderChangeDetection = void 0;
|
|
4
|
+
const aws_cdk_lib_1 = require("aws-cdk-lib");
|
|
5
|
+
const aws_dynamodb_1 = require("aws-cdk-lib/aws-dynamodb");
|
|
6
|
+
const aws_events_1 = require("aws-cdk-lib/aws-events");
|
|
7
|
+
const aws_events_targets_1 = require("aws-cdk-lib/aws-events-targets");
|
|
8
|
+
const aws_lambda_1 = require("aws-cdk-lib/aws-lambda");
|
|
9
|
+
const constructs_1 = require("constructs");
|
|
10
|
+
const path_1 = require("path");
|
|
11
|
+
const cdk_esbuild_1 = require("@aligent/cdk-esbuild");
|
|
12
|
+
const command = [
|
|
13
|
+
"sh",
|
|
14
|
+
"-c",
|
|
15
|
+
'echo "Docker build not supported. Please install esbuild."',
|
|
16
|
+
];
|
|
17
|
+
const defaultHeaders = [
|
|
18
|
+
"content-security-policy",
|
|
19
|
+
"content-security-policy-report-only",
|
|
20
|
+
"reporting-endpoints",
|
|
21
|
+
"strict-transport-security",
|
|
22
|
+
"x-frame-options",
|
|
23
|
+
"x-content-type-options",
|
|
24
|
+
"cross-origin-opener-policy",
|
|
25
|
+
"cross-origin-embedder-policy",
|
|
26
|
+
"cross-origin-resource-policy",
|
|
27
|
+
"referrer-policy",
|
|
28
|
+
"permission-policy",
|
|
29
|
+
"cache-control",
|
|
30
|
+
];
|
|
31
|
+
class HeaderChangeDetection extends constructs_1.Construct {
|
|
32
|
+
constructor(scope, id, props) {
|
|
33
|
+
var _a;
|
|
34
|
+
super(scope, id);
|
|
35
|
+
const headers = props.disableDefaults ? [] : defaultHeaders;
|
|
36
|
+
headers.push(...(((_a = props.additionalHeaders) === null || _a === void 0 ? void 0 : _a.map(header => header.toLowerCase())) || []));
|
|
37
|
+
const table = new aws_dynamodb_1.Table(this, "Table", {
|
|
38
|
+
partitionKey: {
|
|
39
|
+
name: "Url",
|
|
40
|
+
type: aws_dynamodb_1.AttributeType.STRING,
|
|
41
|
+
},
|
|
42
|
+
billingMode: aws_dynamodb_1.BillingMode.PAY_PER_REQUEST,
|
|
43
|
+
});
|
|
44
|
+
const schedule = new aws_events_1.Rule(this, "EventRule", {
|
|
45
|
+
schedule: props.schedule || aws_events_1.Schedule.rate(aws_cdk_lib_1.Duration.hours(1)),
|
|
46
|
+
...props.ruleProps,
|
|
47
|
+
});
|
|
48
|
+
const lambda = new aws_lambda_1.Function(this, "HeaderCheck", {
|
|
49
|
+
architecture: aws_lambda_1.Architecture.X86_64,
|
|
50
|
+
runtime: aws_lambda_1.Runtime.NODEJS_22_X,
|
|
51
|
+
handler: "header-check.handler",
|
|
52
|
+
timeout: props.lambdaTimeout || aws_cdk_lib_1.Duration.seconds(10),
|
|
53
|
+
code: aws_lambda_1.Code.fromAsset((0, path_1.join)(__dirname, "lambda"), {
|
|
54
|
+
bundling: {
|
|
55
|
+
command,
|
|
56
|
+
image: aws_cdk_lib_1.DockerImage.fromRegistry("busybox"),
|
|
57
|
+
local: new cdk_esbuild_1.Esbuild({
|
|
58
|
+
entryPoints: [(0, path_1.join)(__dirname, "lambda/header-check.ts")],
|
|
59
|
+
}),
|
|
60
|
+
},
|
|
61
|
+
}),
|
|
62
|
+
environment: {
|
|
63
|
+
URLS: props.urls.join(","),
|
|
64
|
+
HEADERS: headers.join(","),
|
|
65
|
+
TABLE: table.tableName,
|
|
66
|
+
TOPIC_ARN: props.snsTopic.topic.topicArn,
|
|
67
|
+
ACCEPTED_HTTP_STATUS: (props.acceptedHttpStatus || ["200"]).join(","),
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
schedule.addTarget(new aws_events_targets_1.LambdaFunction(lambda));
|
|
71
|
+
table.grantWriteData(lambda);
|
|
72
|
+
table.grantReadData(lambda);
|
|
73
|
+
props.snsTopic.topic.grantPublish(lambda);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
exports.HeaderChangeDetection = HeaderChangeDetection;
|
|
77
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaGVhZGVyLWNoYW5nZS1kZXRlY3Rpb24uanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyJoZWFkZXItY2hhbmdlLWRldGVjdGlvbi50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiOzs7QUFBQSw2Q0FBb0Q7QUFDcEQsMkRBQTZFO0FBQzdFLHVEQUFtRTtBQUNuRSx1RUFBMEU7QUFDMUUsdURBQStFO0FBQy9FLDJDQUF1QztBQUN2QywrQkFBNEI7QUFDNUIsc0RBQStDO0FBb0QvQyxNQUFNLE9BQU8sR0FBRztJQUNkLElBQUk7SUFDSixJQUFJO0lBQ0osNERBQTREO0NBQzdELENBQUM7QUFFRixNQUFNLGNBQWMsR0FBRztJQUNyQix5QkFBeUI7SUFDekIscUNBQXFDO0lBQ3JDLHFCQUFxQjtJQUNyQiwyQkFBMkI7SUFDM0IsaUJBQWlCO0lBQ2pCLHdCQUF3QjtJQUN4Qiw0QkFBNEI7SUFDNUIsOEJBQThCO0lBQzlCLDhCQUE4QjtJQUM5QixpQkFBaUI7SUFDakIsbUJBQW1CO0lBQ25CLGVBQWU7Q0FDaEIsQ0FBQztBQUVGLE1BQWEscUJBQXNCLFNBQVEsc0JBQVM7SUFDbEQsWUFBWSxLQUFnQixFQUFFLEVBQVUsRUFBRSxLQUFpQzs7UUFDekUsS0FBSyxDQUFDLEtBQUssRUFBRSxFQUFFLENBQUMsQ0FBQztRQUVqQixNQUFNLE9BQU8sR0FBRyxLQUFLLENBQUMsZUFBZSxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsQ0FBQyxDQUFDLGNBQWMsQ0FBQztRQUU1RCxPQUFPLENBQUMsSUFBSSxDQUNWLEdBQUcsQ0FBQyxDQUFBLE1BQUEsS0FBSyxDQUFDLGlCQUFpQiwwQ0FBRSxHQUFHLENBQUMsTUFBTSxDQUFDLEVBQUUsQ0FBQyxNQUFNLENBQUMsV0FBVyxFQUFFLENBQUMsS0FBSSxFQUFFLENBQUMsQ0FDeEUsQ0FBQztRQUVGLE1BQU0sS0FBSyxHQUFHLElBQUksb0JBQUssQ0FBQyxJQUFJLEVBQUUsT0FBTyxFQUFFO1lBQ3JDLFlBQVksRUFBRTtnQkFDWixJQUFJLEVBQUUsS0FBSztnQkFDWCxJQUFJLEVBQUUsNEJBQWEsQ0FBQyxNQUFNO2FBQzNCO1lBQ0QsV0FBVyxFQUFFLDBCQUFXLENBQUMsZUFBZTtTQUN6QyxDQUFDLENBQUM7UUFFSCxNQUFNLFFBQVEsR0FBRyxJQUFJLGlCQUFJLENBQUMsSUFBSSxFQUFFLFdBQVcsRUFBRTtZQUMzQyxRQUFRLEVBQUUsS0FBSyxDQUFDLFFBQVEsSUFBSSxxQkFBUSxDQUFDLElBQUksQ0FBQyxzQkFBUSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsQ0FBQztZQUM1RCxHQUFHLEtBQUssQ0FBQyxTQUFTO1NBQ25CLENBQUMsQ0FBQztRQUVILE1BQU0sTUFBTSxHQUFHLElBQUkscUJBQVEsQ0FBQyxJQUFJLEVBQUUsYUFBYSxFQUFFO1lBQy9DLFlBQVksRUFBRSx5QkFBWSxDQUFDLE1BQU07WUFDakMsT0FBTyxFQUFFLG9CQUFPLENBQUMsV0FBVztZQUM1QixPQUFPLEVBQUUsc0JBQXNCO1lBQy9CLE9BQU8sRUFBRSxLQUFLLENBQUMsYUFBYSxJQUFJLHNCQUFRLENBQUMsT0FBTyxDQUFDLEVBQUUsQ0FBQztZQUNwRCxJQUFJLEVBQUUsaUJBQUksQ0FBQyxTQUFTLENBQUMsSUFBQSxXQUFJLEVBQUMsU0FBUyxFQUFFLFFBQVEsQ0FBQyxFQUFFO2dCQUM5QyxRQUFRLEVBQUU7b0JBQ1IsT0FBTztvQkFDUCxLQUFLLEVBQUUseUJBQVcsQ0FBQyxZQUFZLENBQUMsU0FBUyxDQUFDO29CQUMxQyxLQUFLLEVBQUUsSUFBSSxxQkFBTyxDQUFDO3dCQUNqQixXQUFXLEVBQUUsQ0FBQyxJQUFBLFdBQUksRUFBQyxTQUFTLEVBQUUsd0JBQXdCLENBQUMsQ0FBQztxQkFDekQsQ0FBQztpQkFDSDthQUNGLENBQUM7WUFDRixXQUFXLEVBQUU7Z0JBQ1gsSUFBSSxFQUFFLEtBQUssQ0FBQyxJQUFJLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQztnQkFDMUIsT0FBTyxFQUFFLE9BQU8sQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDO2dCQUMxQixLQUFLLEVBQUUsS0FBSyxDQUFDLFNBQVM7Z0JBQ3RCLFNBQVMsRUFBRSxLQUFLLENBQUMsUUFBUSxDQUFDLEtBQUssQ0FBQyxRQUFRO2dCQUN4QyxvQkFBb0IsRUFBRSxDQUFDLEtBQUssQ0FBQyxrQkFBa0IsSUFBSSxDQUFDLEtBQUssQ0FBQyxDQUFDLENBQUMsSUFBSSxDQUFDLEdBQUcsQ0FBQzthQUN0RTtTQUNGLENBQUMsQ0FBQztRQUVILFFBQVEsQ0FBQyxTQUFTLENBQUMsSUFBSSxtQ0FBYyxDQUFDLE1BQU0sQ0FBQyxDQUFDLENBQUM7UUFFL0MsS0FBSyxDQUFDLGNBQWMsQ0FBQyxNQUFNLENBQUMsQ0FBQztRQUM3QixLQUFLLENBQUMsYUFBYSxDQUFDLE1BQU0sQ0FBQyxDQUFDO1FBQzVCLEtBQUssQ0FBQyxRQUFRLENBQUMsS0FBSyxDQUFDLFlBQVksQ0FBQyxNQUFNLENBQUMsQ0FBQztJQUM1QyxDQUFDO0NBQ0Y7QUFwREQsc0RBb0RDIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgRG9ja2VySW1hZ2UsIER1cmF0aW9uIH0gZnJvbSBcImF3cy1jZGstbGliXCI7XG5pbXBvcnQgeyBBdHRyaWJ1dGVUeXBlLCBCaWxsaW5nTW9kZSwgVGFibGUgfSBmcm9tIFwiYXdzLWNkay1saWIvYXdzLWR5bmFtb2RiXCI7XG5pbXBvcnQgeyBSdWxlLCBSdWxlUHJvcHMsIFNjaGVkdWxlIH0gZnJvbSBcImF3cy1jZGstbGliL2F3cy1ldmVudHNcIjtcbmltcG9ydCB7IExhbWJkYUZ1bmN0aW9uLCBTbnNUb3BpYyB9IGZyb20gXCJhd3MtY2RrLWxpYi9hd3MtZXZlbnRzLXRhcmdldHNcIjtcbmltcG9ydCB7IEFyY2hpdGVjdHVyZSwgQ29kZSwgRnVuY3Rpb24sIFJ1bnRpbWUgfSBmcm9tIFwiYXdzLWNkay1saWIvYXdzLWxhbWJkYVwiO1xuaW1wb3J0IHsgQ29uc3RydWN0IH0gZnJvbSBcImNvbnN0cnVjdHNcIjtcbmltcG9ydCB7IGpvaW4gfSBmcm9tIFwicGF0aFwiO1xuaW1wb3J0IHsgRXNidWlsZCB9IGZyb20gXCJAYWxpZ2VudC9jZGstZXNidWlsZFwiO1xuXG5leHBvcnQgaW50ZXJmYWNlIEhlYWRlckNoYW5nZURldGVjdGlvblByb3BzIHtcbiAgLyoqXG4gICAqIExpc3Qgb2YgVVJMcyB0byBtb25pdG9yIGZvciBoZWFkZXIgY2hhbmdlc1xuICAgKi9cbiAgdXJsczogc3RyaW5nW107XG5cbiAgLyoqXG4gICAqIE9wdGlvbmFsIGxpc3Qgb2YgYWRkaXRpb25hbCBoZWFkZXJzIHRvIG1vbml0b3JcbiAgICpcbiAgICogQGRlZmF1bHQgW11cbiAgICovXG4gIGFkZGl0aW9uYWxIZWFkZXJzPzogc3RyaW5nW107XG5cbiAgLyoqXG4gICAqIE9wdGlvbmFsbHkgZGlzYWJsZSBhbGwgdGhlIGRlZmF1bHQgaGVhZGVyc1xuICAgKlxuICAgKiBAZGVmYXVsdCBmYWxzZVxuICAgKi9cbiAgZGlzYWJsZURlZmF1bHRzPzogYm9vbGVhbjtcblxuICAvKipcbiAgICogU05TIFRvcGljIHRvIHNlbmQgY2hhbmdlIGRldGVjdGlvbiBub3RpZmljYXRpb25zIHRvXG4gICAqL1xuICBzbnNUb3BpYzogU25zVG9waWM7XG5cbiAgLyoqXG4gICAqIFRoZSBzY2hlZHVsZSBmb3IgcGVyZm9ybWluZyB0aGUgaGVhZGVyIGNoZWNrXG4gICAqXG4gICAqIEBkZWZhdWx0IFNjaGVkdWxlLnJhdGUoRHVyYXRpb24uaG91cnMoMSkpXG4gICAqL1xuICBzY2hlZHVsZT86IFNjaGVkdWxlO1xuXG4gIC8qKlxuICAgKiBPcHRpb25hbGx5IHBhc3MgYW55IHJ1bGUgcHJvcGVydGllc1xuICAgKi9cbiAgcnVsZVByb3BzPzogUGFydGlhbDxSdWxlUHJvcHM+O1xuXG4gIC8qKlxuICAgKiBPcHRpb25hbGx5IGFjY2VwdCBIVFRQIHN0YXR1cyBjb2RlcyBvdGhlciB0aGFuIDIwMFxuICAgKlxuICAgKiBAZGVmYXVsdCBbXCIyMDBcIl1cbiAgICovXG4gIGFjY2VwdGVkSHR0cFN0YXR1cz86IHN0cmluZ1tdO1xuXG4gIC8qKlxuICAgKiBGb3IgZXh0ZW5kZWQgTGFtYmRhIHRpbWVvdXQuIERlZmF1bHQ6IDEwIHNlY29uZHNcbiAgICovXG4gIGxhbWJkYVRpbWVvdXQ/OiBEdXJhdGlvbjtcbn1cblxuY29uc3QgY29tbWFuZCA9IFtcbiAgXCJzaFwiLFxuICBcIi1jXCIsXG4gICdlY2hvIFwiRG9ja2VyIGJ1aWxkIG5vdCBzdXBwb3J0ZWQuIFBsZWFzZSBpbnN0YWxsIGVzYnVpbGQuXCInLFxuXTtcblxuY29uc3QgZGVmYXVsdEhlYWRlcnMgPSBbXG4gIFwiY29udGVudC1zZWN1cml0eS1wb2xpY3lcIixcbiAgXCJjb250ZW50LXNlY3VyaXR5LXBvbGljeS1yZXBvcnQtb25seVwiLFxuICBcInJlcG9ydGluZy1lbmRwb2ludHNcIixcbiAgXCJzdHJpY3QtdHJhbnNwb3J0LXNlY3VyaXR5XCIsXG4gIFwieC1mcmFtZS1vcHRpb25zXCIsXG4gIFwieC1jb250ZW50LXR5cGUtb3B0aW9uc1wiLFxuICBcImNyb3NzLW9yaWdpbi1vcGVuZXItcG9saWN5XCIsXG4gIFwiY3Jvc3Mtb3JpZ2luLWVtYmVkZGVyLXBvbGljeVwiLFxuICBcImNyb3NzLW9yaWdpbi1yZXNvdXJjZS1wb2xpY3lcIixcbiAgXCJyZWZlcnJlci1wb2xpY3lcIixcbiAgXCJwZXJtaXNzaW9uLXBvbGljeVwiLFxuICBcImNhY2hlLWNvbnRyb2xcIixcbl07XG5cbmV4cG9ydCBjbGFzcyBIZWFkZXJDaGFuZ2VEZXRlY3Rpb24gZXh0ZW5kcyBDb25zdHJ1Y3Qge1xuICBjb25zdHJ1Y3RvcihzY29wZTogQ29uc3RydWN0LCBpZDogc3RyaW5nLCBwcm9wczogSGVhZGVyQ2hhbmdlRGV0ZWN0aW9uUHJvcHMpIHtcbiAgICBzdXBlcihzY29wZSwgaWQpO1xuXG4gICAgY29uc3QgaGVhZGVycyA9IHByb3BzLmRpc2FibGVEZWZhdWx0cyA/IFtdIDogZGVmYXVsdEhlYWRlcnM7XG5cbiAgICBoZWFkZXJzLnB1c2goXG4gICAgICAuLi4ocHJvcHMuYWRkaXRpb25hbEhlYWRlcnM/Lm1hcChoZWFkZXIgPT4gaGVhZGVyLnRvTG93ZXJDYXNlKCkpIHx8IFtdKVxuICAgICk7XG5cbiAgICBjb25zdCB0YWJsZSA9IG5ldyBUYWJsZSh0aGlzLCBcIlRhYmxlXCIsIHtcbiAgICAgIHBhcnRpdGlvbktleToge1xuICAgICAgICBuYW1lOiBcIlVybFwiLFxuICAgICAgICB0eXBlOiBBdHRyaWJ1dGVUeXBlLlNUUklORyxcbiAgICAgIH0sXG4gICAgICBiaWxsaW5nTW9kZTogQmlsbGluZ01vZGUuUEFZX1BFUl9SRVFVRVNULFxuICAgIH0pO1xuXG4gICAgY29uc3Qgc2NoZWR1bGUgPSBuZXcgUnVsZSh0aGlzLCBcIkV2ZW50UnVsZVwiLCB7XG4gICAgICBzY2hlZHVsZTogcHJvcHMuc2NoZWR1bGUgfHwgU2NoZWR1bGUucmF0ZShEdXJhdGlvbi5ob3VycygxKSksXG4gICAgICAuLi5wcm9wcy5ydWxlUHJvcHMsXG4gICAgfSk7XG5cbiAgICBjb25zdCBsYW1iZGEgPSBuZXcgRnVuY3Rpb24odGhpcywgXCJIZWFkZXJDaGVja1wiLCB7XG4gICAgICBhcmNoaXRlY3R1cmU6IEFyY2hpdGVjdHVyZS5YODZfNjQsXG4gICAgICBydW50aW1lOiBSdW50aW1lLk5PREVKU18yMl9YLFxuICAgICAgaGFuZGxlcjogXCJoZWFkZXItY2hlY2suaGFuZGxlclwiLFxuICAgICAgdGltZW91dDogcHJvcHMubGFtYmRhVGltZW91dCB8fCBEdXJhdGlvbi5zZWNvbmRzKDEwKSxcbiAgICAgIGNvZGU6IENvZGUuZnJvbUFzc2V0KGpvaW4oX19kaXJuYW1lLCBcImxhbWJkYVwiKSwge1xuICAgICAgICBidW5kbGluZzoge1xuICAgICAgICAgIGNvbW1hbmQsXG4gICAgICAgICAgaW1hZ2U6IERvY2tlckltYWdlLmZyb21SZWdpc3RyeShcImJ1c3lib3hcIiksXG4gICAgICAgICAgbG9jYWw6IG5ldyBFc2J1aWxkKHtcbiAgICAgICAgICAgIGVudHJ5UG9pbnRzOiBbam9pbihfX2Rpcm5hbWUsIFwibGFtYmRhL2hlYWRlci1jaGVjay50c1wiKV0sXG4gICAgICAgICAgfSksXG4gICAgICAgIH0sXG4gICAgICB9KSxcbiAgICAgIGVudmlyb25tZW50OiB7XG4gICAgICAgIFVSTFM6IHByb3BzLnVybHMuam9pbihcIixcIiksXG4gICAgICAgIEhFQURFUlM6IGhlYWRlcnMuam9pbihcIixcIiksXG4gICAgICAgIFRBQkxFOiB0YWJsZS50YWJsZU5hbWUsXG4gICAgICAgIFRPUElDX0FSTjogcHJvcHMuc25zVG9waWMudG9waWMudG9waWNBcm4sXG4gICAgICAgIEFDQ0VQVEVEX0hUVFBfU1RBVFVTOiAocHJvcHMuYWNjZXB0ZWRIdHRwU3RhdHVzIHx8IFtcIjIwMFwiXSkuam9pbihcIixcIiksXG4gICAgICB9LFxuICAgIH0pO1xuXG4gICAgc2NoZWR1bGUuYWRkVGFyZ2V0KG5ldyBMYW1iZGFGdW5jdGlvbihsYW1iZGEpKTtcblxuICAgIHRhYmxlLmdyYW50V3JpdGVEYXRhKGxhbWJkYSk7XG4gICAgdGFibGUuZ3JhbnRSZWFkRGF0YShsYW1iZGEpO1xuICAgIHByb3BzLnNuc1RvcGljLnRvcGljLmdyYW50UHVibGlzaChsYW1iZGEpO1xuICB9XG59XG4iXX0=
|
package/package.json
CHANGED
package/index.ts
DELETED
package/jest.config.ts
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/* eslint-disable */
|
|
2
|
-
export default {
|
|
3
|
-
displayName: "header-change-detection",
|
|
4
|
-
preset: "../../jest.preset.js",
|
|
5
|
-
testEnvironment: "node",
|
|
6
|
-
transform: {
|
|
7
|
-
"^.+\\.[tj]s$": ["ts-jest", { tsconfig: "<rootDir>/tsconfig.spec.json" }],
|
|
8
|
-
},
|
|
9
|
-
moduleFileExtensions: ["ts", "js", "html"],
|
|
10
|
-
coverageDirectory: "../../coverage/packages/header-change-detection",
|
|
11
|
-
};
|
|
@@ -1,133 +0,0 @@
|
|
|
1
|
-
import { DockerImage, Duration } from "aws-cdk-lib";
|
|
2
|
-
import { AttributeType, BillingMode, Table } from "aws-cdk-lib/aws-dynamodb";
|
|
3
|
-
import { Rule, RuleProps, Schedule } from "aws-cdk-lib/aws-events";
|
|
4
|
-
import { LambdaFunction, SnsTopic } from "aws-cdk-lib/aws-events-targets";
|
|
5
|
-
import { Architecture, Code, Function, Runtime } from "aws-cdk-lib/aws-lambda";
|
|
6
|
-
import { Construct } from "constructs";
|
|
7
|
-
import { join } from "path";
|
|
8
|
-
import { Esbuild } from "@aligent/cdk-esbuild";
|
|
9
|
-
|
|
10
|
-
export interface HeaderChangeDetectionProps {
|
|
11
|
-
/**
|
|
12
|
-
* List of URLs to monitor for header changes
|
|
13
|
-
*/
|
|
14
|
-
urls: string[];
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* Optional list of additional headers to monitor
|
|
18
|
-
*
|
|
19
|
-
* @default []
|
|
20
|
-
*/
|
|
21
|
-
additionalHeaders?: string[];
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Optionally disable all the default headers
|
|
25
|
-
*
|
|
26
|
-
* @default false
|
|
27
|
-
*/
|
|
28
|
-
disableDefaults?: boolean;
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* SNS Topic to send change detection notifications to
|
|
32
|
-
*/
|
|
33
|
-
snsTopic: SnsTopic;
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* The schedule for performing the header check
|
|
37
|
-
*
|
|
38
|
-
* @default Schedule.rate(Duration.hours(1))
|
|
39
|
-
*/
|
|
40
|
-
schedule?: Schedule;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Optionally pass any rule properties
|
|
44
|
-
*/
|
|
45
|
-
ruleProps?: Partial<RuleProps>;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Optionally accept HTTP status codes other than 200
|
|
49
|
-
*
|
|
50
|
-
* @default ["200"]
|
|
51
|
-
*/
|
|
52
|
-
acceptedHttpStatus?: string[];
|
|
53
|
-
|
|
54
|
-
/**
|
|
55
|
-
* For extended Lambda timeout. Default: 10 seconds
|
|
56
|
-
*/
|
|
57
|
-
lambdaTimeout?: Duration;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const command = [
|
|
61
|
-
"sh",
|
|
62
|
-
"-c",
|
|
63
|
-
'echo "Docker build not supported. Please install esbuild."',
|
|
64
|
-
];
|
|
65
|
-
|
|
66
|
-
const defaultHeaders = [
|
|
67
|
-
"content-security-policy",
|
|
68
|
-
"content-security-policy-report-only",
|
|
69
|
-
"reporting-endpoints",
|
|
70
|
-
"strict-transport-security",
|
|
71
|
-
"x-frame-options",
|
|
72
|
-
"x-content-type-options",
|
|
73
|
-
"cross-origin-opener-policy",
|
|
74
|
-
"cross-origin-embedder-policy",
|
|
75
|
-
"cross-origin-resource-policy",
|
|
76
|
-
"referrer-policy",
|
|
77
|
-
"permission-policy",
|
|
78
|
-
"cache-control",
|
|
79
|
-
];
|
|
80
|
-
|
|
81
|
-
export class HeaderChangeDetection extends Construct {
|
|
82
|
-
constructor(scope: Construct, id: string, props: HeaderChangeDetectionProps) {
|
|
83
|
-
super(scope, id);
|
|
84
|
-
|
|
85
|
-
const headers = props.disableDefaults ? [] : defaultHeaders;
|
|
86
|
-
|
|
87
|
-
headers.push(
|
|
88
|
-
...(props.additionalHeaders?.map(header => header.toLowerCase()) || [])
|
|
89
|
-
);
|
|
90
|
-
|
|
91
|
-
const table = new Table(this, "Table", {
|
|
92
|
-
partitionKey: {
|
|
93
|
-
name: "Url",
|
|
94
|
-
type: AttributeType.STRING,
|
|
95
|
-
},
|
|
96
|
-
billingMode: BillingMode.PAY_PER_REQUEST,
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
const schedule = new Rule(this, "EventRule", {
|
|
100
|
-
schedule: props.schedule || Schedule.rate(Duration.hours(1)),
|
|
101
|
-
...props.ruleProps,
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
const lambda = new Function(this, "HeaderCheck", {
|
|
105
|
-
architecture: Architecture.X86_64,
|
|
106
|
-
runtime: Runtime.NODEJS_22_X,
|
|
107
|
-
handler: "header-check.handler",
|
|
108
|
-
timeout: props.lambdaTimeout || Duration.seconds(10),
|
|
109
|
-
code: Code.fromAsset(join(__dirname, "lambda"), {
|
|
110
|
-
bundling: {
|
|
111
|
-
command,
|
|
112
|
-
image: DockerImage.fromRegistry("busybox"),
|
|
113
|
-
local: new Esbuild({
|
|
114
|
-
entryPoints: [join(__dirname, "lambda/header-check.ts")],
|
|
115
|
-
}),
|
|
116
|
-
},
|
|
117
|
-
}),
|
|
118
|
-
environment: {
|
|
119
|
-
URLS: props.urls.join(","),
|
|
120
|
-
HEADERS: headers.join(","),
|
|
121
|
-
TABLE: table.tableName,
|
|
122
|
-
TOPIC_ARN: props.snsTopic.topic.topicArn,
|
|
123
|
-
ACCEPTED_HTTP_STATUS: (props.acceptedHttpStatus || ["200"]).join(","),
|
|
124
|
-
},
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
schedule.addTarget(new LambdaFunction(lambda));
|
|
128
|
-
|
|
129
|
-
table.grantWriteData(lambda);
|
|
130
|
-
table.grantReadData(lambda);
|
|
131
|
-
props.snsTopic.topic.grantPublish(lambda);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
@@ -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
|
-
};
|
package/project.json
DELETED
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "header-change-detection",
|
|
3
|
-
"$schema": "../../node_modules/nx/schemas/project-schema.json",
|
|
4
|
-
"sourceRoot": "packages/header-change-detection/lib",
|
|
5
|
-
"projectType": "application",
|
|
6
|
-
"targets": {
|
|
7
|
-
"build": {
|
|
8
|
-
"executor": "@nx/js:tsc",
|
|
9
|
-
"options": {
|
|
10
|
-
"main": "packages/header-change-detection/index.ts",
|
|
11
|
-
"outputPath": "dist/header-change-detection",
|
|
12
|
-
"tsConfig": "packages/header-change-detection/tsconfig.app.json",
|
|
13
|
-
"assets": [
|
|
14
|
-
"packages/header-change-detection/lib/lambda/**",
|
|
15
|
-
"packages/header-change-detection/README.md",
|
|
16
|
-
"packages/header-change-detection/docs/**"
|
|
17
|
-
]
|
|
18
|
-
},
|
|
19
|
-
"dependsOn": ["merge-gitignore"]
|
|
20
|
-
},
|
|
21
|
-
"lint": {
|
|
22
|
-
"executor": "@nx/eslint:lint",
|
|
23
|
-
"outputs": ["{options.outputFile}"]
|
|
24
|
-
},
|
|
25
|
-
"test": {
|
|
26
|
-
"executor": "@nx/jest:jest",
|
|
27
|
-
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
|
|
28
|
-
"options": {
|
|
29
|
-
"jestConfig": "packages/header-change-detection/jest.config.ts",
|
|
30
|
-
"passWithNoTests": true
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
"publish": {
|
|
34
|
-
"command": "node tools/scripts/publish.mjs header-change-detection {args.ver} {args.tag}",
|
|
35
|
-
"dependsOn": ["build"]
|
|
36
|
-
},
|
|
37
|
-
"merge-gitignore": {
|
|
38
|
-
"executor": "nx:run-commands",
|
|
39
|
-
"options": {
|
|
40
|
-
"command": "node tools/scripts/merge-gitignore.mjs header-change-detection"
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
},
|
|
44
|
-
"tags": []
|
|
45
|
-
}
|
package/tsconfig.app.json
DELETED
package/tsconfig.json
DELETED