@kensio/yulin 1.22.0 → 1.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/serve/controller/sim-service-controller.d.ts +6 -0
- package/dist/serve/controller/sim-service-controller.js +7 -0
- package/dist/serve/http/request/sim-aws-request-authenticator.js +3 -0
- package/dist/service/apigateway/serve/sim-api-gateway-controller.js +1 -0
- package/dist/service/apigateway/serve/sim-rest-api-web-acl-inspection.d.ts +2 -0
- package/dist/service/apigateway/serve/sim-rest-api-web-acl-inspection.js +1 -0
- package/dist/service/cognito/serve/sim-cognito-controller.js +4 -3
- package/dist/service/cognito/serve/sim-cognito-domain-controller.js +2 -0
- package/dist/service/cognito/serve/sim-cognito-web-acl-inspection.d.ts +2 -0
- package/dist/service/cognito/serve/sim-cognito-web-acl-inspection.js +1 -0
- package/dist/service/iam/index.d.ts +1 -0
- package/dist/service/iam/index.js +1 -0
- package/dist/service/iam/request/error/sim-aws-request-auth.error.d.ts +1 -1
- package/dist/service/iam/request/error/sim-aws-request-auth.error.js +1 -1
- package/dist/service/iam/request/sim-aws-control-headers.d.ts +2 -1
- package/dist/service/iam/request/sim-aws-control-headers.js +4 -1
- package/dist/service/iam/request/sim-aws-source-ip-header.d.ts +19 -0
- package/dist/service/iam/request/sim-aws-source-ip-header.js +32 -0
- package/dist/service/wafv2/association/sim-waf-associations.js +1 -1
- package/dist/service/wafv2/association/sim-waf-protection.d.ts +5 -0
- package/dist/service/wafv2/association/sim-waf-request-inspection.d.ts +2 -0
- package/dist/service/wafv2/association/sim-waf-request-inspection.js +7 -2
- package/dist/service/wafv2/evaluate/sim-waf-evaluation-request.d.ts +5 -0
- package/dist/service/wafv2/evaluate/sim-waf-inspected-request.d.ts +12 -1
- package/dist/service/wafv2/evaluate/sim-waf-inspected-request.js +12 -1
- package/dist/service/wafv2/index.d.ts +2 -1
- package/dist/service/wafv2/index.js +1 -0
- package/dist/service/wafv2/managed/group/sim-waf-ip-reputation.d.ts +8 -5
- package/dist/service/wafv2/managed/group/sim-waf-ip-reputation.js +10 -5
- package/dist/service/wafv2/managed/sim-waf-managed-address-declaration.d.ts +32 -0
- package/dist/service/wafv2/managed/sim-waf-managed-address-declaration.js +52 -0
- package/dist/service/wafv2/managed/sim-waf-managed-address-range.d.ts +21 -0
- package/dist/service/wafv2/managed/sim-waf-managed-address-range.js +73 -0
- package/dist/service/wafv2/managed/sim-waf-managed-rule-groups.d.ts +7 -0
- package/dist/service/wafv2/managed/sim-waf-managed-rule-groups.js +15 -0
- package/dist/service/wafv2/managed/sim-waf-managed-rule.type.d.ts +5 -0
- package/dist/service/wafv2/managed/sim-waf-managed-rules.d.ts +27 -1
- package/dist/service/wafv2/managed/sim-waf-managed-rules.js +33 -13
- package/dist/service/wafv2/sim-wafv2.js +2 -1
- package/dist/service/wafv2/statement/sim-waf-rate-based-input.d.ts +5 -4
- package/dist/service/wafv2/statement/sim-waf-rate-based-input.js +6 -6
- package/docs/services/iam/README.md +6 -0
- package/docs/services/wafv2/README.md +105 -14
- package/package.json +3 -3
|
@@ -25,6 +25,7 @@ interface SimAwsServiceRequestProperties {
|
|
|
25
25
|
readonly request: Request;
|
|
26
26
|
readonly caller?: SimAwsRequestCaller | undefined;
|
|
27
27
|
readonly source?: SimAwsRequestSource | undefined;
|
|
28
|
+
readonly sourceIp?: string | undefined;
|
|
28
29
|
readonly body?: Uint8Array | undefined;
|
|
29
30
|
}
|
|
30
31
|
/**
|
|
@@ -49,6 +50,11 @@ export declare class SimAwsServiceRequest {
|
|
|
49
50
|
* conditioned on, and most requests state none.
|
|
50
51
|
*/
|
|
51
52
|
readonly source: SimAwsRequestSource | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* The client address the request came from, which is loopback unless the
|
|
55
|
+
* request stated another with `x-sim-aws-source-ip`.
|
|
56
|
+
*/
|
|
57
|
+
readonly sourceIp: string;
|
|
52
58
|
readonly body?: Uint8Array | undefined;
|
|
53
59
|
constructor(properties: SimAwsServiceRequestProperties);
|
|
54
60
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { simAwsProxiedSourceIp } from "../http/sim-aws-proxied-connection.js";
|
|
1
2
|
import { SimAwsRequestCaller } from "../../service/iam/request/sim-aws-request-caller.js";
|
|
2
3
|
/**
|
|
3
4
|
* One HTTP request, routed to a simulated service and attributed to a
|
|
@@ -21,6 +22,11 @@ export class SimAwsServiceRequest {
|
|
|
21
22
|
* conditioned on, and most requests state none.
|
|
22
23
|
*/
|
|
23
24
|
source;
|
|
25
|
+
/**
|
|
26
|
+
* The client address the request came from, which is loopback unless the
|
|
27
|
+
* request stated another with `x-sim-aws-source-ip`.
|
|
28
|
+
*/
|
|
29
|
+
sourceIp;
|
|
24
30
|
body;
|
|
25
31
|
constructor(properties) {
|
|
26
32
|
this.target = properties.target;
|
|
@@ -28,6 +34,7 @@ export class SimAwsServiceRequest {
|
|
|
28
34
|
// An unattributed request is anonymous, never the Account root.
|
|
29
35
|
this.caller = properties.caller ?? SimAwsRequestCaller.anonymous();
|
|
30
36
|
this.source = properties.source;
|
|
37
|
+
this.sourceIp = properties.sourceIp ?? simAwsProxiedSourceIp;
|
|
31
38
|
this.body = properties.body;
|
|
32
39
|
}
|
|
33
40
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { simAwsServiceSigningName } from "../../controller/sim-aws-service-signing-name.js";
|
|
2
2
|
import { SimAwsServiceRequest, } from "../../controller/sim-service-controller.js";
|
|
3
3
|
import { SimAwsRequestSource } from "../../../service/iam/request/sim-aws-request-source.js";
|
|
4
|
+
import { simAwsRequestSourceIp } from "../../../service/iam/request/sim-aws-source-ip-header.js";
|
|
4
5
|
import { SimAwsReceivedRequest } from "./sim-aws-received-request.js";
|
|
5
6
|
/**
|
|
6
7
|
* Turns a received HTTP request into a request a simulated service can serve.
|
|
@@ -40,11 +41,13 @@ export class SimAwsRequestAuthenticator {
|
|
|
40
41
|
},
|
|
41
42
|
});
|
|
42
43
|
const source = SimAwsRequestSource.fromHeaders(request.headers);
|
|
44
|
+
const sourceIp = simAwsRequestSourceIp(request.headers);
|
|
43
45
|
return new SimAwsServiceRequest({
|
|
44
46
|
target,
|
|
45
47
|
request: received.forService(),
|
|
46
48
|
caller,
|
|
47
49
|
...(source !== undefined && { source }),
|
|
50
|
+
sourceIp,
|
|
48
51
|
body: received.body,
|
|
49
52
|
});
|
|
50
53
|
}
|
|
@@ -5,6 +5,8 @@ interface SimRestApiInspectionInput {
|
|
|
5
5
|
/** The first path segment of the request, which names the stage. */
|
|
6
6
|
readonly stageName: string;
|
|
7
7
|
readonly request: Request;
|
|
8
|
+
/** The client address the request came from. */
|
|
9
|
+
readonly sourceIp: string;
|
|
8
10
|
}
|
|
9
11
|
/**
|
|
10
12
|
* Puts a request to the web ACL in front of the stage it reached.
|
|
@@ -69,7 +69,7 @@ export class SimCognitoServiceController {
|
|
|
69
69
|
// The web ACL sits in front of the endpoint rather than behind it, so a
|
|
70
70
|
// request it blocks is answered before the method is judged. A write the
|
|
71
71
|
// rules turned away gets WAF's 403 and never learns the endpoint reads.
|
|
72
|
-
const blocked = await this.blocked(pool, segment,
|
|
72
|
+
const blocked = await this.blocked(pool, segment, serviceRequest);
|
|
73
73
|
if (blocked !== undefined) {
|
|
74
74
|
return blocked;
|
|
75
75
|
}
|
|
@@ -90,14 +90,15 @@ export class SimCognitoServiceController {
|
|
|
90
90
|
* A blocked request is answered before the document is read, so a verifier
|
|
91
91
|
* the rules turn away gets 403 and no keys.
|
|
92
92
|
*/
|
|
93
|
-
async blocked(pool, segment,
|
|
93
|
+
async blocked(pool, segment, serviceRequest) {
|
|
94
94
|
if (!simCognitoProtectedSegment(segment)) {
|
|
95
95
|
return undefined;
|
|
96
96
|
}
|
|
97
97
|
const inspected = await this.webAcl.inspect({
|
|
98
98
|
pool,
|
|
99
99
|
cognito: simCognitoPoolScope(this.simAws, pool),
|
|
100
|
-
request,
|
|
100
|
+
request: serviceRequest.request,
|
|
101
|
+
sourceIp: serviceRequest.sourceIp,
|
|
101
102
|
});
|
|
102
103
|
return inspected.blocked;
|
|
103
104
|
}
|
|
@@ -44,6 +44,7 @@ export class SimCognitoDomainController {
|
|
|
44
44
|
pool,
|
|
45
45
|
cognito,
|
|
46
46
|
request: serviceRequest.request,
|
|
47
|
+
sourceIp: serviceRequest.sourceIp,
|
|
47
48
|
});
|
|
48
49
|
if (inspected.blocked !== undefined) {
|
|
49
50
|
return inspected.blocked;
|
|
@@ -95,6 +96,7 @@ function forwarded(serviceRequest, request) {
|
|
|
95
96
|
request,
|
|
96
97
|
caller: serviceRequest.caller,
|
|
97
98
|
source: serviceRequest.source,
|
|
99
|
+
sourceIp: serviceRequest.sourceIp,
|
|
98
100
|
body: serviceRequest.body,
|
|
99
101
|
});
|
|
100
102
|
}
|
|
@@ -6,6 +6,8 @@ interface SimCognitoInspectionInput {
|
|
|
6
6
|
/** The simulated Cognito scope the pool belongs to. */
|
|
7
7
|
readonly cognito: SimCognitoIdentityProvider;
|
|
8
8
|
readonly request: Request;
|
|
9
|
+
/** The client address the request came from. */
|
|
10
|
+
readonly sourceIp: string;
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* Puts a request to the web ACL in front of the pool it reached.
|
|
@@ -9,4 +9,5 @@ export type { SimIamSigV4ExpectedScope } from "./sigv4/sim-iam-sigv4-expected-sc
|
|
|
9
9
|
export { simAwsCallerHeaderName, simAwsCallerHeaderPrincipal, simAwsCallerHeaderValue, } from "./request/sim-aws-caller-header.js";
|
|
10
10
|
export { type SimAwsRequestAuthMethod, SimAwsRequestCaller, } from "./request/sim-aws-request-caller.js";
|
|
11
11
|
export { SimAwsRequestSource, simAwsSourceAccountHeaderName, simAwsSourceArnHeaderName, } from "./request/sim-aws-request-source.js";
|
|
12
|
+
export { simAwsRequestSourceIp, simAwsSourceIpHeaderName, } from "./request/sim-aws-source-ip-header.js";
|
|
12
13
|
export { isSimAwsRequestAuthFailure, SimAwsInvalidCallerHeader, type SimAwsRequestAuthFailure, } from "./request/error/sim-aws-request-auth.error.js";
|
|
@@ -4,4 +4,5 @@ export { SimIamCredentialScopeMismatch, SimIamExpiredToken, SimIamIncompleteSign
|
|
|
4
4
|
export { simAwsCallerHeaderName, simAwsCallerHeaderPrincipal, simAwsCallerHeaderValue, } from "./request/sim-aws-caller-header.js";
|
|
5
5
|
export { SimAwsRequestCaller, } from "./request/sim-aws-request-caller.js";
|
|
6
6
|
export { SimAwsRequestSource, simAwsSourceAccountHeaderName, simAwsSourceArnHeaderName, } from "./request/sim-aws-request-source.js";
|
|
7
|
+
export { simAwsRequestSourceIp, simAwsSourceIpHeaderName, } from "./request/sim-aws-source-ip-header.js";
|
|
7
8
|
export { isSimAwsRequestAuthFailure, SimAwsInvalidCallerHeader, } from "./request/error/sim-aws-request-auth.error.js";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SimIamSigV4Error } from "../../sigv4/error/sim-iam-sigv4.error.js";
|
|
2
2
|
/**
|
|
3
3
|
* Raised when the `x-sim-aws-caller` header does not name a principal the
|
|
4
|
-
* simulator understands.
|
|
4
|
+
* simulator understands, or `x-sim-aws-source-ip` names no address.
|
|
5
5
|
*
|
|
6
6
|
* This is a simulator control header rather than anything real AWS defines, so
|
|
7
7
|
* a bad value is the caller's mistake in using Yulin, not a failed AWS
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SimIamSigV4Error } from "../../sigv4/error/sim-iam-sigv4.error.js";
|
|
2
2
|
/**
|
|
3
3
|
* Raised when the `x-sim-aws-caller` header does not name a principal the
|
|
4
|
-
* simulator understands.
|
|
4
|
+
* simulator understands, or `x-sim-aws-source-ip` names no address.
|
|
5
5
|
*
|
|
6
6
|
* This is a simulator control header rather than anything real AWS defines, so
|
|
7
7
|
* a bad value is the caller's mistake in using Yulin, not a failed AWS
|
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* Header names that are instructions to the simulator rather than part of the
|
|
3
3
|
* request being simulated.
|
|
4
4
|
*
|
|
5
|
-
* They state who a request is from and what it is for, so
|
|
5
|
+
* They state who a request is from, where it came from and what it is for, so
|
|
6
|
+
* anything passing a
|
|
6
7
|
* request on to a simulated service has to decide what to do with them rather
|
|
7
8
|
* than forward them by accident: the HTTP boundary strips them once the
|
|
8
9
|
* request has been attributed, and sim CloudFront strips a viewer's before
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { simAwsCallerHeaderName } from "./sim-aws-caller-header.js";
|
|
2
|
+
import { simAwsSourceIpHeaderName } from "./sim-aws-source-ip-header.js";
|
|
2
3
|
import { simAwsSourceAccountHeaderName, simAwsSourceArnHeaderName, } from "./sim-aws-request-source.js";
|
|
3
4
|
/**
|
|
4
5
|
* Header names that are instructions to the simulator rather than part of the
|
|
5
6
|
* request being simulated.
|
|
6
7
|
*
|
|
7
|
-
* They state who a request is from and what it is for, so
|
|
8
|
+
* They state who a request is from, where it came from and what it is for, so
|
|
9
|
+
* anything passing a
|
|
8
10
|
* request on to a simulated service has to decide what to do with them rather
|
|
9
11
|
* than forward them by accident: the HTTP boundary strips them once the
|
|
10
12
|
* request has been attributed, and sim CloudFront strips a viewer's before
|
|
@@ -14,6 +16,7 @@ export const simAwsControlHeaderNames = [
|
|
|
14
16
|
simAwsCallerHeaderName,
|
|
15
17
|
simAwsSourceArnHeaderName,
|
|
16
18
|
simAwsSourceAccountHeaderName,
|
|
19
|
+
simAwsSourceIpHeaderName,
|
|
17
20
|
];
|
|
18
21
|
/**
|
|
19
22
|
* Remove the simulator's control headers from a set of request headers.
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The header naming the client address a request into simulated AWS comes
|
|
3
|
+
* from.
|
|
4
|
+
*
|
|
5
|
+
* A request arrives in process or over loopback, so there is no remote address
|
|
6
|
+
* for the simulation to read. Whatever decides by where a request came from,
|
|
7
|
+
* such as a web ACL's IP reputation and anonymous IP rules, reads this instead,
|
|
8
|
+
* and a test sends it to say which client a request stands for.
|
|
9
|
+
*/
|
|
10
|
+
export declare const simAwsSourceIpHeaderName = "x-sim-aws-source-ip";
|
|
11
|
+
/**
|
|
12
|
+
* Read the client address a request states, or the loopback address when it
|
|
13
|
+
* states none.
|
|
14
|
+
*
|
|
15
|
+
* The header holds one IPv4 or IPv6 address. A value that is neither is
|
|
16
|
+
* refused rather than read as loopback, since a test that misspelt an address
|
|
17
|
+
* would otherwise pass for the wrong reason.
|
|
18
|
+
*/
|
|
19
|
+
export declare function simAwsRequestSourceIp(headers: Headers): string;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import { simAwsProxiedSourceIp } from "../../../serve/http/sim-aws-proxied-connection.js";
|
|
3
|
+
import { SimAwsInvalidCallerHeader } from "./error/sim-aws-request-auth.error.js";
|
|
4
|
+
/**
|
|
5
|
+
* The header naming the client address a request into simulated AWS comes
|
|
6
|
+
* from.
|
|
7
|
+
*
|
|
8
|
+
* A request arrives in process or over loopback, so there is no remote address
|
|
9
|
+
* for the simulation to read. Whatever decides by where a request came from,
|
|
10
|
+
* such as a web ACL's IP reputation and anonymous IP rules, reads this instead,
|
|
11
|
+
* and a test sends it to say which client a request stands for.
|
|
12
|
+
*/
|
|
13
|
+
export const simAwsSourceIpHeaderName = "x-sim-aws-source-ip";
|
|
14
|
+
/**
|
|
15
|
+
* Read the client address a request states, or the loopback address when it
|
|
16
|
+
* states none.
|
|
17
|
+
*
|
|
18
|
+
* The header holds one IPv4 or IPv6 address. A value that is neither is
|
|
19
|
+
* refused rather than read as loopback, since a test that misspelt an address
|
|
20
|
+
* would otherwise pass for the wrong reason.
|
|
21
|
+
*/
|
|
22
|
+
export function simAwsRequestSourceIp(headers) {
|
|
23
|
+
const value = headers.get(simAwsSourceIpHeaderName)?.trim();
|
|
24
|
+
if (value === undefined || value === "") {
|
|
25
|
+
return simAwsProxiedSourceIp;
|
|
26
|
+
}
|
|
27
|
+
if (isIP(value) === 0) {
|
|
28
|
+
throw new SimAwsInvalidCallerHeader(`${simAwsSourceIpHeaderName} value ${value} must be one IPv4 or IPv6 ` +
|
|
29
|
+
`address`);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
}
|
|
@@ -75,7 +75,7 @@ export class SimWafAssociations {
|
|
|
75
75
|
if (association === undefined) {
|
|
76
76
|
return undefined;
|
|
77
77
|
}
|
|
78
|
-
return association.webAcl.evaluate(simWafInspectedRequest(request.request, request.body, association.webAcl.bodyInspectionLimitBytes(association.resourceType)));
|
|
78
|
+
return association.webAcl.evaluate(simWafInspectedRequest(request.request, request.body, association.webAcl.bodyInspectionLimitBytes(association.resourceType), request.sourceIp));
|
|
79
79
|
}
|
|
80
80
|
/**
|
|
81
81
|
* Forget whatever is in front of one resource.
|
|
@@ -6,6 +6,11 @@ export interface SimWafProtectedRequest {
|
|
|
6
6
|
/** The resource the request reached, by ARN. */
|
|
7
7
|
readonly resourceArn: string;
|
|
8
8
|
readonly request: Request;
|
|
9
|
+
/**
|
|
10
|
+
* The client address the request came from, which is loopback when nothing
|
|
11
|
+
* says otherwise.
|
|
12
|
+
*/
|
|
13
|
+
readonly sourceIp?: string | undefined;
|
|
9
14
|
/**
|
|
10
15
|
* The request body, when the rules might inspect it. A request body is a
|
|
11
16
|
* stream that cannot be read twice, so it is passed in already buffered.
|
|
@@ -5,6 +5,8 @@ interface SimWafInspectionInput {
|
|
|
5
5
|
/** The resource the request reached, by ARN. */
|
|
6
6
|
readonly resourceArn: string;
|
|
7
7
|
readonly request: Request;
|
|
8
|
+
/** The client address the request came from. */
|
|
9
|
+
readonly sourceIp: string;
|
|
8
10
|
/**
|
|
9
11
|
* Whether the fronting service forwards the request body to AWS WAF.
|
|
10
12
|
*
|
|
@@ -27,12 +27,17 @@ export class SimWafRequestInspection {
|
|
|
27
27
|
* Put one request to whatever protects the resource it addressed.
|
|
28
28
|
*/
|
|
29
29
|
async inspect(input) {
|
|
30
|
-
const { protection, resourceArn, request } = input;
|
|
30
|
+
const { protection, resourceArn, request, sourceIp } = input;
|
|
31
31
|
if (!protection.protects(resourceArn)) {
|
|
32
32
|
return new SimWafInspected({ request });
|
|
33
33
|
}
|
|
34
34
|
const body = input.forwardBody ? await inspectedBody(request) : undefined;
|
|
35
|
-
const decision = protection.decide({
|
|
35
|
+
const decision = protection.decide({
|
|
36
|
+
resourceArn,
|
|
37
|
+
request,
|
|
38
|
+
sourceIp,
|
|
39
|
+
body,
|
|
40
|
+
});
|
|
36
41
|
// A decision carries a blocked response when its action was to block, and
|
|
37
42
|
// carries none when the request was allowed. That one reading is what
|
|
38
43
|
// decides the request here.
|
|
@@ -6,6 +6,11 @@ export interface SimWafEvaluationRequest {
|
|
|
6
6
|
/** The web ACL to evaluate against, by ARN. */
|
|
7
7
|
readonly webAclArn: string;
|
|
8
8
|
readonly request: Request;
|
|
9
|
+
/**
|
|
10
|
+
* The client address the request came from. A request naming none comes
|
|
11
|
+
* from `127.0.0.1`, as every request does that states no other address.
|
|
12
|
+
*/
|
|
13
|
+
readonly sourceIp?: string | undefined;
|
|
9
14
|
/**
|
|
10
15
|
* The request body, when the rules might inspect it. A request body is a
|
|
11
16
|
* stream that cannot be read twice, so it is passed in already buffered.
|
|
@@ -20,6 +20,14 @@ export interface SimWafInspectedRequest {
|
|
|
20
20
|
* header with the Yulin-local suffix taken off it.
|
|
21
21
|
*/
|
|
22
22
|
readonly host: string;
|
|
23
|
+
/**
|
|
24
|
+
* The client address the request came from, which is what the IP-based
|
|
25
|
+
* rules read.
|
|
26
|
+
*
|
|
27
|
+
* Nothing reaches this simulation over a public network, so this is
|
|
28
|
+
* `127.0.0.1` unless the request said otherwise with `x-sim-aws-source-ip`.
|
|
29
|
+
*/
|
|
30
|
+
readonly sourceIp: string;
|
|
23
31
|
/** The request body, or nothing when the request carried none. */
|
|
24
32
|
readonly body: Uint8Array | undefined;
|
|
25
33
|
/**
|
|
@@ -49,5 +57,8 @@ export interface SimWafInspectedRequest {
|
|
|
49
57
|
* The body inspection limit defaults to the 16 KB every resource type this
|
|
50
58
|
* simulation protects reads by default. A web ACL raising it with
|
|
51
59
|
* `AssociationConfig` passes the raised figure in.
|
|
60
|
+
*
|
|
61
|
+
* The client address defaults to loopback, which is where a request comes from
|
|
62
|
+
* when it states no other address.
|
|
52
63
|
*/
|
|
53
|
-
export declare function simWafInspectedRequest(request: Request, body?: Uint8Array, bodyInspectionLimitBytes?: number): SimWafInspectedRequest;
|
|
64
|
+
export declare function simWafInspectedRequest(request: Request, body?: Uint8Array, bodyInspectionLimitBytes?: number, sourceIp?: string): SimWafInspectedRequest;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
|
+
import { isIP } from "node:net";
|
|
2
|
+
import { simAwsProxiedSourceIp } from "../../../serve/http/sim-aws-proxied-connection.js";
|
|
1
3
|
import { simAwsRequestHostname } from "../../../serve/http/url/sim-aws-request-hostname.js";
|
|
4
|
+
import { SimWafDeclarationError } from "../error/sim-wafv2.error.js";
|
|
2
5
|
import { simWafBodyInspectionLimitBytes } from "../web-acl/sim-waf-association-config.js";
|
|
3
6
|
import { SimWafRequestLabels } from "./sim-waf-request-labels.js";
|
|
4
7
|
/**
|
|
@@ -16,8 +19,15 @@ import { SimWafRequestLabels } from "./sim-waf-request-labels.js";
|
|
|
16
19
|
* The body inspection limit defaults to the 16 KB every resource type this
|
|
17
20
|
* simulation protects reads by default. A web ACL raising it with
|
|
18
21
|
* `AssociationConfig` passes the raised figure in.
|
|
22
|
+
*
|
|
23
|
+
* The client address defaults to loopback, which is where a request comes from
|
|
24
|
+
* when it states no other address.
|
|
19
25
|
*/
|
|
20
|
-
export function simWafInspectedRequest(request, body, bodyInspectionLimitBytes = simWafBodyInspectionLimitBytes) {
|
|
26
|
+
export function simWafInspectedRequest(request, body, bodyInspectionLimitBytes = simWafBodyInspectionLimitBytes, sourceIp = simAwsProxiedSourceIp) {
|
|
27
|
+
if (isIP(sourceIp) === 0) {
|
|
28
|
+
throw new SimWafDeclarationError(`A request's source address must be one IPv4 or IPv6 address, and ` +
|
|
29
|
+
`${sourceIp} is neither`);
|
|
30
|
+
}
|
|
21
31
|
const url = new URL(request.url);
|
|
22
32
|
return {
|
|
23
33
|
method: request.method,
|
|
@@ -28,6 +38,7 @@ export function simWafInspectedRequest(request, body, bodyInspectionLimitBytes =
|
|
|
28
38
|
queryString: url.search.replace(/^\?/u, ""),
|
|
29
39
|
headers: request.headers,
|
|
30
40
|
host: simAwsRequestHostname(request),
|
|
41
|
+
sourceIp,
|
|
31
42
|
body,
|
|
32
43
|
bodyInspectionLimitBytes,
|
|
33
44
|
labels: new SimWafRequestLabels(),
|
|
@@ -25,13 +25,14 @@ export { SimWafIpSet, type SimWafIpAddressVersion, } from "./ip-set/sim-waf-ip-s
|
|
|
25
25
|
export { SimWafRegexPatternSet } from "./regex-pattern-set/sim-waf-regex-pattern-set.js";
|
|
26
26
|
export type { SimWafDecision } from "./evaluate/sim-waf-decision.js";
|
|
27
27
|
export { SimWafRequestLabels } from "./evaluate/sim-waf-request-labels.js";
|
|
28
|
-
export { SimWafManagedRules, type SimWafManagedMatchDeclaration, } from "./managed/sim-waf-managed-rules.js";
|
|
28
|
+
export { SimWafManagedRules, type SimWafManagedAddressDeclaration, type SimWafManagedMatchDeclaration, } from "./managed/sim-waf-managed-rules.js";
|
|
29
29
|
export type { SimWafBotDeclaration } from "./managed/sim-waf-bot-declaration.js";
|
|
30
30
|
export type { SimWafManagedRuleReport } from "./managed/sim-waf-managed-rule-report.js";
|
|
31
31
|
export type { SimWafManagedRuleTier } from "./managed/sim-waf-managed-rule.type.js";
|
|
32
32
|
export type { SimWafManagedRuleGroupStatementInput, SimWafOverrideActionInput, SimWafRuleActionOverrideInput, } from "./managed/sim-waf-managed-group.type.js";
|
|
33
33
|
export { simWafBlockedHttpResponse, type SimWafBlockedResponse, simWafDefaultBlockedResponse, } from "./evaluate/sim-waf-blocked-response.js";
|
|
34
34
|
export { type SimWafInspectedRequest, simWafInspectedRequest, } from "./evaluate/sim-waf-inspected-request.js";
|
|
35
|
+
export { simAwsSourceIpHeaderName } from "../iam/request/sim-aws-source-ip-header.js";
|
|
35
36
|
export { simWafHeaderInspectionLimitBytes } from "./statement/sim-waf-field-content.js";
|
|
36
37
|
export { type SimWafAssociationConfigInput, type SimWafBodyInspectionResourceType, simWafBodyInspectionLimitBytes, } from "./web-acl/sim-waf-association-config.js";
|
|
37
38
|
export type { SimWafRateBasedStatementInput } from "./statement/sim-waf-rate-based.type.js";
|
|
@@ -20,6 +20,7 @@ export { SimWafRequestLabels } from "./evaluate/sim-waf-request-labels.js";
|
|
|
20
20
|
export { SimWafManagedRules, } from "./managed/sim-waf-managed-rules.js";
|
|
21
21
|
export { simWafBlockedHttpResponse, simWafDefaultBlockedResponse, } from "./evaluate/sim-waf-blocked-response.js";
|
|
22
22
|
export { simWafInspectedRequest, } from "./evaluate/sim-waf-inspected-request.js";
|
|
23
|
+
export { simAwsSourceIpHeaderName } from "../iam/request/sim-aws-source-ip-header.js";
|
|
23
24
|
export { simWafHeaderInspectionLimitBytes } from "./statement/sim-waf-field-content.js";
|
|
24
25
|
export { simWafBodyInspectionLimitBytes, } from "./web-acl/sim-waf-association-config.js";
|
|
25
26
|
export { SimWafAssociatedItemException, SimWafDeclarationError, SimWafDuplicateItemException, SimWafError, SimWafInvalidParameterException, SimWafNonexistentItemException, SimWafOptimisticLockException, SimWafUnavailableEntityException, SimWafUnsimulatedInputException, } from "./error/sim-wafv2.error.js";
|
|
@@ -3,16 +3,19 @@ import type { SimWafManagedRuleGroupDefinition } from "../sim-waf-managed-rule.t
|
|
|
3
3
|
* The Amazon IP reputation list, which decides by the address a request came
|
|
4
4
|
* from.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* acts on
|
|
6
|
+
* AWS publishes none of the addresses on its lists, so every rule here is
|
|
7
|
+
* declared-only. A test says which addresses a rule flags with
|
|
8
|
+
* `managedRules().onAddress`, and sends a request from one of them with the
|
|
9
|
+
* `x-sim-aws-source-ip` header. The group then labels and acts on it as AWS
|
|
10
|
+
* documents. `managedRules().onRequest` declares a match by path instead.
|
|
10
11
|
*/
|
|
11
12
|
export declare const simWafIpReputationList: SimWafManagedRuleGroupDefinition;
|
|
12
13
|
/**
|
|
13
14
|
* The anonymous IP list, for VPNs, proxies, Tor nodes and hosting providers.
|
|
14
15
|
*
|
|
15
16
|
* It decides by source address as the reputation list does, and every rule is
|
|
16
|
-
* declared-only for the same reason.
|
|
17
|
+
* declared-only for the same reason. `AnonymousIPList` stands for the VPNs,
|
|
18
|
+
* proxies and Tor nodes, and `HostingProviderIPList` for the hosting and cloud
|
|
19
|
+
* providers other than AWS.
|
|
17
20
|
*/
|
|
18
21
|
export declare const simWafAnonymousIpList: SimWafManagedRuleGroupDefinition;
|
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
* The Amazon IP reputation list, which decides by the address a request came
|
|
3
3
|
* from.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* acts on
|
|
5
|
+
* AWS publishes none of the addresses on its lists, so every rule here is
|
|
6
|
+
* declared-only. A test says which addresses a rule flags with
|
|
7
|
+
* `managedRules().onAddress`, and sends a request from one of them with the
|
|
8
|
+
* `x-sim-aws-source-ip` header. The group then labels and acts on it as AWS
|
|
9
|
+
* documents. `managedRules().onRequest` declares a match by path instead.
|
|
9
10
|
*/
|
|
10
11
|
export const simWafIpReputationList = {
|
|
11
12
|
name: "AWSManagedRulesAmazonIpReputationList",
|
|
12
13
|
labelNamespace: "awswaf:managed:aws:amazon-ip-list",
|
|
13
14
|
capacity: 25,
|
|
15
|
+
readsSourceAddress: true,
|
|
14
16
|
rules: [
|
|
15
17
|
{
|
|
16
18
|
name: "AWSManagedIPReputationList",
|
|
@@ -34,12 +36,15 @@ export const simWafIpReputationList = {
|
|
|
34
36
|
* The anonymous IP list, for VPNs, proxies, Tor nodes and hosting providers.
|
|
35
37
|
*
|
|
36
38
|
* It decides by source address as the reputation list does, and every rule is
|
|
37
|
-
* declared-only for the same reason.
|
|
39
|
+
* declared-only for the same reason. `AnonymousIPList` stands for the VPNs,
|
|
40
|
+
* proxies and Tor nodes, and `HostingProviderIPList` for the hosting and cloud
|
|
41
|
+
* providers other than AWS.
|
|
38
42
|
*/
|
|
39
43
|
export const simWafAnonymousIpList = {
|
|
40
44
|
name: "AWSManagedRulesAnonymousIpList",
|
|
41
45
|
labelNamespace: "awswaf:managed:aws:anonymous-ip-list",
|
|
42
46
|
capacity: 50,
|
|
47
|
+
readsSourceAddress: true,
|
|
43
48
|
rules: [
|
|
44
49
|
{ name: "AnonymousIPList", label: "AnonymousIPList", tier: "declared" },
|
|
45
50
|
{
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { SimWafDeclared } from "./sim-waf-managed-declaration.js";
|
|
2
|
+
/**
|
|
3
|
+
* The managed rules a test says flag the requests from some addresses.
|
|
4
|
+
*/
|
|
5
|
+
export interface SimWafManagedAddressDeclaration {
|
|
6
|
+
/**
|
|
7
|
+
* The rules that flag the addresses, by the name AWS gives them, such as
|
|
8
|
+
* `AnonymousIPList` or `HostingProviderIPList`.
|
|
9
|
+
*/
|
|
10
|
+
readonly matches: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* The address ranges a test declared, each with the rules that flag a request
|
|
14
|
+
* from inside it.
|
|
15
|
+
*
|
|
16
|
+
* Declaring the same range again replaces what it was declared with, and a
|
|
17
|
+
* request from inside two declared ranges is flagged by the rules of both.
|
|
18
|
+
*/
|
|
19
|
+
export declare class SimWafDeclaredAddresses {
|
|
20
|
+
#private;
|
|
21
|
+
/**
|
|
22
|
+
* Declare the rules that flag the requests from one address or CIDR range.
|
|
23
|
+
*
|
|
24
|
+
* Each rule is checked to be one that decides by the client address.
|
|
25
|
+
*/
|
|
26
|
+
declare(range: string, ruleNames: readonly string[]): void;
|
|
27
|
+
/**
|
|
28
|
+
* What was declared for a request's path, joined with what was declared
|
|
29
|
+
* for the address it came from.
|
|
30
|
+
*/
|
|
31
|
+
joinedWith(declared: SimWafDeclared, sourceIp: string): SimWafDeclared;
|
|
32
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { SimWafDeclarationError } from "../error/sim-wafv2.error.js";
|
|
2
|
+
import { SimWafDeclaredAddressRange } from "./sim-waf-managed-address-range.js";
|
|
3
|
+
import { requiredSimWafManagedRule, } from "./sim-waf-managed-rule-groups.js";
|
|
4
|
+
/**
|
|
5
|
+
* The address ranges a test declared, each with the rules that flag a request
|
|
6
|
+
* from inside it.
|
|
7
|
+
*
|
|
8
|
+
* Declaring the same range again replaces what it was declared with, and a
|
|
9
|
+
* request from inside two declared ranges is flagged by the rules of both.
|
|
10
|
+
*/
|
|
11
|
+
export class SimWafDeclaredAddresses {
|
|
12
|
+
#byRange = new Map();
|
|
13
|
+
/**
|
|
14
|
+
* Declare the rules that flag the requests from one address or CIDR range.
|
|
15
|
+
*
|
|
16
|
+
* Each rule is checked to be one that decides by the client address.
|
|
17
|
+
*/
|
|
18
|
+
declare(range, ruleNames) {
|
|
19
|
+
const matches = new Set(ruleNames.map((name) => simWafAddressRuleName(requiredSimWafManagedRule(name))));
|
|
20
|
+
this.#byRange.set(range.trim(), SimWafDeclaredAddressRange.read(range, matches));
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* What was declared for a request's path, joined with what was declared
|
|
24
|
+
* for the address it came from.
|
|
25
|
+
*/
|
|
26
|
+
joinedWith(declared, sourceIp) {
|
|
27
|
+
const byAddress = this.#byRange
|
|
28
|
+
.values()
|
|
29
|
+
.filter((range) => range.holds(sourceIp))
|
|
30
|
+
.flatMap((range) => range.matches)
|
|
31
|
+
.toArray();
|
|
32
|
+
if (byAddress.length === 0) {
|
|
33
|
+
return declared;
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
matches: new Set([...declared.matches, ...byAddress]),
|
|
37
|
+
groupLabels: declared.groupLabels,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The name of a rule a test may declare by address, refusing one whose group
|
|
43
|
+
* reads the request rather than where it came from.
|
|
44
|
+
*/
|
|
45
|
+
function simWafAddressRuleName(entry) {
|
|
46
|
+
if (entry.group.readsSourceAddress !== true) {
|
|
47
|
+
throw new SimWafDeclarationError(`${entry.rule.name} belongs to ${entry.group.name}, which does not ` +
|
|
48
|
+
`decide by the client address, so a match for it is declared by path ` +
|
|
49
|
+
`with managedRules().onRequest rather than by address.`);
|
|
50
|
+
}
|
|
51
|
+
return entry.rule.name;
|
|
52
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One address or range of addresses, and the rules that flag a request from
|
|
3
|
+
* inside it.
|
|
4
|
+
*/
|
|
5
|
+
export declare class SimWafDeclaredAddressRange {
|
|
6
|
+
#private;
|
|
7
|
+
readonly matches: ReadonlySet<string>;
|
|
8
|
+
private constructor();
|
|
9
|
+
/**
|
|
10
|
+
* Read one range a test declared, refusing what is not an address or a CIDR
|
|
11
|
+
* range.
|
|
12
|
+
*/
|
|
13
|
+
static read(range: string, matches: ReadonlySet<string>): SimWafDeclaredAddressRange;
|
|
14
|
+
/**
|
|
15
|
+
* Whether one client address falls inside this range.
|
|
16
|
+
*
|
|
17
|
+
* An IPv4 address is never inside an IPv6 range, nor the reverse, which is
|
|
18
|
+
* how an AWS IP set holding one kind treats the other.
|
|
19
|
+
*/
|
|
20
|
+
holds(sourceIp: string): boolean;
|
|
21
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { BlockList, isIP } from "node:net";
|
|
2
|
+
import { SimWafDeclarationError } from "../error/sim-wafv2.error.js";
|
|
3
|
+
/**
|
|
4
|
+
* One address or range of addresses, and the rules that flag a request from
|
|
5
|
+
* inside it.
|
|
6
|
+
*/
|
|
7
|
+
export class SimWafDeclaredAddressRange {
|
|
8
|
+
matches;
|
|
9
|
+
#addresses = new BlockList();
|
|
10
|
+
constructor(range, matches) {
|
|
11
|
+
this.matches = matches;
|
|
12
|
+
if (range.prefix === undefined) {
|
|
13
|
+
this.#addresses.addAddress(range.address, range.family);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
this.#addresses.addSubnet(range.address, range.prefix, range.family);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Read one range a test declared, refusing what is not an address or a CIDR
|
|
21
|
+
* range.
|
|
22
|
+
*/
|
|
23
|
+
static read(range, matches) {
|
|
24
|
+
return new SimWafDeclaredAddressRange(checkedRange(range), matches);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Whether one client address falls inside this range.
|
|
28
|
+
*
|
|
29
|
+
* An IPv4 address is never inside an IPv6 range, nor the reverse, which is
|
|
30
|
+
* how an AWS IP set holding one kind treats the other.
|
|
31
|
+
*/
|
|
32
|
+
holds(sourceIp) {
|
|
33
|
+
const family = familyOf(sourceIp);
|
|
34
|
+
return family !== undefined && this.#addresses.check(sourceIp, family);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Read an address or a CIDR range into the address, the prefix length and the
|
|
39
|
+
* family.
|
|
40
|
+
*/
|
|
41
|
+
function checkedRange(range) {
|
|
42
|
+
const [address = "", prefixText, ...rest] = range.trim().split("/");
|
|
43
|
+
const family = familyOf(address);
|
|
44
|
+
if (family === undefined || rest.length > 0) {
|
|
45
|
+
throw invalidRange(range);
|
|
46
|
+
}
|
|
47
|
+
if (prefixText === undefined) {
|
|
48
|
+
return { address, prefix: undefined, family };
|
|
49
|
+
}
|
|
50
|
+
const prefix = Number(prefixText);
|
|
51
|
+
const widest = family === "ipv4" ? 32 : 128;
|
|
52
|
+
if (!/^\d+$/u.test(prefixText) || prefix > widest) {
|
|
53
|
+
throw invalidRange(range);
|
|
54
|
+
}
|
|
55
|
+
return { address, prefix, family };
|
|
56
|
+
}
|
|
57
|
+
function familyOf(address) {
|
|
58
|
+
switch (isIP(address)) {
|
|
59
|
+
case 4: {
|
|
60
|
+
return "ipv4";
|
|
61
|
+
}
|
|
62
|
+
case 6: {
|
|
63
|
+
return "ipv6";
|
|
64
|
+
}
|
|
65
|
+
default: {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function invalidRange(range) {
|
|
71
|
+
return new SimWafDeclarationError(`A declared address match needs one IPv4 or IPv6 address, or a CIDR ` +
|
|
72
|
+
`range such as 185.220.101.0/24, and ${range} is neither`);
|
|
73
|
+
}
|
|
@@ -30,6 +30,13 @@ export declare function simWafManagedRuleGroupNames(): readonly string[];
|
|
|
30
30
|
* declared match and an action override name a rule and nothing else.
|
|
31
31
|
*/
|
|
32
32
|
export declare function findSimWafManagedRule(name: string): SimWafManagedRuleEntry | undefined;
|
|
33
|
+
/**
|
|
34
|
+
* Find one managed rule by name, refusing a name no simulated group holds.
|
|
35
|
+
*
|
|
36
|
+
* A declaration naming a rule that is absent is refused where it is made,
|
|
37
|
+
* since it would otherwise sit there matching nothing.
|
|
38
|
+
*/
|
|
39
|
+
export declare function requiredSimWafManagedRule(name: string): SimWafManagedRuleEntry;
|
|
33
40
|
/**
|
|
34
41
|
* Every rule of every simulated group, in the order the groups evaluate them.
|
|
35
42
|
*/
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { SimWafDeclarationError } from "../error/sim-wafv2.error.js";
|
|
1
2
|
import { simWafAdminProtectionRuleSet } from "./group/sim-waf-admin-protection.js";
|
|
2
3
|
import { simWafBotControlRuleSet } from "./group/sim-waf-bot-control.js";
|
|
3
4
|
import { simWafCommonRuleSet } from "./group/sim-waf-common-rule-set.js";
|
|
@@ -45,6 +46,20 @@ export function simWafManagedRuleGroupNames() {
|
|
|
45
46
|
export function findSimWafManagedRule(name) {
|
|
46
47
|
return rulesByName.get(name);
|
|
47
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Find one managed rule by name, refusing a name no simulated group holds.
|
|
51
|
+
*
|
|
52
|
+
* A declaration naming a rule that is absent is refused where it is made,
|
|
53
|
+
* since it would otherwise sit there matching nothing.
|
|
54
|
+
*/
|
|
55
|
+
export function requiredSimWafManagedRule(name) {
|
|
56
|
+
const entry = rulesByName.get(name);
|
|
57
|
+
if (entry === undefined) {
|
|
58
|
+
throw new SimWafDeclarationError(`No simulated AWS managed rule group holds a rule named ${name}. The ` +
|
|
59
|
+
`rules that are simulated are reported by managedRules().rules().`);
|
|
60
|
+
}
|
|
61
|
+
return entry;
|
|
62
|
+
}
|
|
48
63
|
/**
|
|
49
64
|
* Every rule of every simulated group, in the order the groups evaluate them.
|
|
50
65
|
*/
|
|
@@ -52,5 +52,10 @@ export interface SimWafManagedRuleGroupDefinition {
|
|
|
52
52
|
readonly labelNamespace: string;
|
|
53
53
|
/** The WCUs the group costs, which `DescribeManagedRuleGroup` reports. */
|
|
54
54
|
readonly capacity: number;
|
|
55
|
+
/**
|
|
56
|
+
* Whether the group decides by the client address alone, which is what lets
|
|
57
|
+
* a test declare its matches by address with `managedRules().onAddress`.
|
|
58
|
+
*/
|
|
59
|
+
readonly readsSourceAddress?: boolean | undefined;
|
|
55
60
|
readonly rules: readonly SimWafManagedRuleDefinition[];
|
|
56
61
|
}
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { SimWafInspectedRequest } from "../evaluate/sim-waf-inspected-request.js";
|
|
2
2
|
import { type SimWafManagedRuleReport } from "./sim-waf-managed-rule-report.js";
|
|
3
3
|
import type { SimWafManagedRuleTier } from "./sim-waf-managed-rule.type.js";
|
|
4
|
+
import { type SimWafManagedAddressDeclaration } from "./sim-waf-managed-address-declaration.js";
|
|
4
5
|
import { type SimWafDeclared, type SimWafManagedMatchDeclaration } from "./sim-waf-managed-declaration.js";
|
|
5
6
|
export type { SimWafDeclared, SimWafManagedMatchDeclaration, } from "./sim-waf-managed-declaration.js";
|
|
7
|
+
export type { SimWafManagedAddressDeclaration } from "./sim-waf-managed-address-declaration.js";
|
|
6
8
|
/**
|
|
7
9
|
* The managed rule matches this simulated WAFv2 answers with, and what it
|
|
8
10
|
* covers of each rule.
|
|
@@ -28,6 +30,16 @@ export type { SimWafDeclared, SimWafManagedMatchDeclaration, } from "./sim-waf-m
|
|
|
28
30
|
* });
|
|
29
31
|
* ```
|
|
30
32
|
*
|
|
33
|
+
* The IP reputation and anonymous IP groups decide by the client address, so
|
|
34
|
+
* their matches can be declared by address instead, for a request that states
|
|
35
|
+
* where it came from with the `x-sim-aws-source-ip` header:
|
|
36
|
+
*
|
|
37
|
+
* ```typescript
|
|
38
|
+
* simAws.wafV2().managedRules().onAddress("185.220.101.0/24", {
|
|
39
|
+
* matches: ["AnonymousIPList"],
|
|
40
|
+
* });
|
|
41
|
+
* ```
|
|
42
|
+
*
|
|
31
43
|
* A declared match is a match, and everything after it is what the group would
|
|
32
44
|
* have done anyway: the rule adds its label, an action override applies to it,
|
|
33
45
|
* and the group blocks by that rule.
|
|
@@ -42,9 +54,24 @@ export declare class SimWafManagedRules {
|
|
|
42
54
|
* is refused here rather than matching nothing later.
|
|
43
55
|
*/
|
|
44
56
|
onRequest(uriPath: string, declaration: SimWafManagedMatchDeclaration): void;
|
|
57
|
+
/**
|
|
58
|
+
* Declare which managed rules flag the requests from one address or one
|
|
59
|
+
* CIDR range of addresses.
|
|
60
|
+
*
|
|
61
|
+
* Only the rules of a group that decides by the client address can be
|
|
62
|
+
* declared this way, which is the IP reputation list and the anonymous IP
|
|
63
|
+
* list. A request states its address with the `x-sim-aws-source-ip` header,
|
|
64
|
+
* and one that states none comes from `127.0.0.1`.
|
|
65
|
+
*
|
|
66
|
+
* Declaring the same range again replaces it.
|
|
67
|
+
*/
|
|
68
|
+
onAddress(range: string, declaration: SimWafManagedAddressDeclaration): void;
|
|
45
69
|
/**
|
|
46
70
|
* What a test declared about one request: the rules that claim it, and the
|
|
47
71
|
* labels a group adds to it whatever its rules decide.
|
|
72
|
+
*
|
|
73
|
+
* A request is claimed by what was declared for its path and for its
|
|
74
|
+
* address together.
|
|
48
75
|
*/
|
|
49
76
|
declared(request: SimWafInspectedRequest): SimWafDeclared;
|
|
50
77
|
/**
|
|
@@ -59,5 +86,4 @@ export declare class SimWafManagedRules {
|
|
|
59
86
|
* AWS rule they stand for.
|
|
60
87
|
*/
|
|
61
88
|
rules(): readonly SimWafManagedRuleReport[];
|
|
62
|
-
private requiredRule;
|
|
63
89
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { SimWafDeclarationError } from "../error/sim-wafv2.error.js";
|
|
2
|
-
import {
|
|
2
|
+
import { requiredSimWafManagedRule } from "./sim-waf-managed-rule-groups.js";
|
|
3
3
|
import { simWafManagedRuleReports, } from "./sim-waf-managed-rule-report.js";
|
|
4
|
+
import { SimWafDeclaredAddresses, } from "./sim-waf-managed-address-declaration.js";
|
|
4
5
|
import { simWafNothingDeclared, simWafReadDeclaration, } from "./sim-waf-managed-declaration.js";
|
|
5
6
|
/**
|
|
6
7
|
* The managed rule matches this simulated WAFv2 answers with, and what it
|
|
@@ -27,12 +28,23 @@ import { simWafNothingDeclared, simWafReadDeclaration, } from "./sim-waf-managed
|
|
|
27
28
|
* });
|
|
28
29
|
* ```
|
|
29
30
|
*
|
|
31
|
+
* The IP reputation and anonymous IP groups decide by the client address, so
|
|
32
|
+
* their matches can be declared by address instead, for a request that states
|
|
33
|
+
* where it came from with the `x-sim-aws-source-ip` header:
|
|
34
|
+
*
|
|
35
|
+
* ```typescript
|
|
36
|
+
* simAws.wafV2().managedRules().onAddress("185.220.101.0/24", {
|
|
37
|
+
* matches: ["AnonymousIPList"],
|
|
38
|
+
* });
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
30
41
|
* A declared match is a match, and everything after it is what the group would
|
|
31
42
|
* have done anyway: the rule adds its label, an action override applies to it,
|
|
32
43
|
* and the group blocks by that rule.
|
|
33
44
|
*/
|
|
34
45
|
export class SimWafManagedRules {
|
|
35
46
|
#byUriPath = new Map();
|
|
47
|
+
#byAddress = new SimWafDeclaredAddresses();
|
|
36
48
|
/**
|
|
37
49
|
* Declare which managed rules claim a request to one URI path.
|
|
38
50
|
*
|
|
@@ -45,20 +57,37 @@ export class SimWafManagedRules {
|
|
|
45
57
|
throw new SimWafDeclarationError("A declared managed rule match needs the URI path of the request it " +
|
|
46
58
|
"is declared for");
|
|
47
59
|
}
|
|
48
|
-
this.#byUriPath.set(uriPath, simWafReadDeclaration(declaration, (name) =>
|
|
60
|
+
this.#byUriPath.set(uriPath, simWafReadDeclaration(declaration, (name) => requiredSimWafManagedRule(name).rule.name));
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Declare which managed rules flag the requests from one address or one
|
|
64
|
+
* CIDR range of addresses.
|
|
65
|
+
*
|
|
66
|
+
* Only the rules of a group that decides by the client address can be
|
|
67
|
+
* declared this way, which is the IP reputation list and the anonymous IP
|
|
68
|
+
* list. A request states its address with the `x-sim-aws-source-ip` header,
|
|
69
|
+
* and one that states none comes from `127.0.0.1`.
|
|
70
|
+
*
|
|
71
|
+
* Declaring the same range again replaces it.
|
|
72
|
+
*/
|
|
73
|
+
onAddress(range, declaration) {
|
|
74
|
+
this.#byAddress.declare(range, declaration.matches);
|
|
49
75
|
}
|
|
50
76
|
/**
|
|
51
77
|
* What a test declared about one request: the rules that claim it, and the
|
|
52
78
|
* labels a group adds to it whatever its rules decide.
|
|
79
|
+
*
|
|
80
|
+
* A request is claimed by what was declared for its path and for its
|
|
81
|
+
* address together.
|
|
53
82
|
*/
|
|
54
83
|
declared(request) {
|
|
55
|
-
return this.#byUriPath.get(request.uriPath) ?? simWafNothingDeclared;
|
|
84
|
+
return this.#byAddress.joinedWith(this.#byUriPath.get(request.uriPath) ?? simWafNothingDeclared, request.sourceIp);
|
|
56
85
|
}
|
|
57
86
|
/**
|
|
58
87
|
* What this simulation covers of one managed rule.
|
|
59
88
|
*/
|
|
60
89
|
tierOf(ruleName) {
|
|
61
|
-
return
|
|
90
|
+
return requiredSimWafManagedRule(ruleName).rule.tier;
|
|
62
91
|
}
|
|
63
92
|
/**
|
|
64
93
|
* What this simulation covers of every managed rule it carries.
|
|
@@ -70,13 +99,4 @@ export class SimWafManagedRules {
|
|
|
70
99
|
rules() {
|
|
71
100
|
return simWafManagedRuleReports();
|
|
72
101
|
}
|
|
73
|
-
requiredRule(ruleName) {
|
|
74
|
-
const entry = findSimWafManagedRule(ruleName);
|
|
75
|
-
if (entry === undefined) {
|
|
76
|
-
throw new SimWafDeclarationError(`No simulated AWS managed rule group holds a rule named ` +
|
|
77
|
-
`${ruleName}. The rules that are simulated are reported by ` +
|
|
78
|
-
`managedRules().rules().`);
|
|
79
|
-
}
|
|
80
|
-
return entry;
|
|
81
|
-
}
|
|
82
102
|
}
|
|
@@ -98,7 +98,8 @@ export class SimWafV2 extends SimWafSets {
|
|
|
98
98
|
evaluateRequest(evaluation) {
|
|
99
99
|
const webAcl = this.requireWebAcl(evaluation.webAclArn);
|
|
100
100
|
const limitBytes = webAcl.bodyInspectionLimitBytes(evaluation.resourceType);
|
|
101
|
-
|
|
101
|
+
const { request, body, sourceIp } = evaluation;
|
|
102
|
+
return webAcl.evaluate(simWafInspectedRequest(request, body, limitBytes, sourceIp));
|
|
102
103
|
}
|
|
103
104
|
/**
|
|
104
105
|
* What the AWS managed rule groups do here, and what a test declares of
|
|
@@ -36,10 +36,11 @@ export declare function simWafRateWindowMilliseconds(statement: SimWafRateBasedS
|
|
|
36
36
|
/**
|
|
37
37
|
* Read how a rate-based statement groups the requests it counts.
|
|
38
38
|
*
|
|
39
|
-
* `IP` counts each client address on its own.
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
39
|
+
* `IP` counts each client address on its own. A request comes from
|
|
40
|
+
* `127.0.0.1` unless it states another address with `x-sim-aws-source-ip`, so
|
|
41
|
+
* a test that states none has one client for the whole simulation. That is
|
|
42
|
+
* the case a rate limiting test is usually written about: one client, sending
|
|
43
|
+
* until the rule trips.
|
|
43
44
|
*
|
|
44
45
|
* `CONSTANT` counts every request the statement sees together, and AWS asks
|
|
45
46
|
* for a scope-down statement alongside it to say which requests those are. A
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { simAwsProxiedSourceIp } from "../../../serve/http/sim-aws-proxied-connection.js";
|
|
2
1
|
import { invalidSimWafRule } from "./sim-waf-rule-refusals.js";
|
|
3
2
|
import { refuseUnsimulatedSimWafRateKeyType } from "./sim-waf-unsimulated-rate-based.js";
|
|
4
3
|
/**
|
|
@@ -66,10 +65,11 @@ export function simWafRateWindowMilliseconds(statement, ruleName) {
|
|
|
66
65
|
/**
|
|
67
66
|
* Read how a rate-based statement groups the requests it counts.
|
|
68
67
|
*
|
|
69
|
-
* `IP` counts each client address on its own.
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
68
|
+
* `IP` counts each client address on its own. A request comes from
|
|
69
|
+
* `127.0.0.1` unless it states another address with `x-sim-aws-source-ip`, so
|
|
70
|
+
* a test that states none has one client for the whole simulation. That is
|
|
71
|
+
* the case a rate limiting test is usually written about: one client, sending
|
|
72
|
+
* until the rule trips.
|
|
73
73
|
*
|
|
74
74
|
* `CONSTANT` counts every request the statement sees together, and AWS asks
|
|
75
75
|
* for a scope-down statement alongside it to say which requests those are. A
|
|
@@ -79,7 +79,7 @@ export function simWafRateAggregation(statement, ruleName) {
|
|
|
79
79
|
const keyType = statement.AggregateKeyType;
|
|
80
80
|
refuseUnsimulatedSimWafRateKeyType(keyType, ruleName);
|
|
81
81
|
if (keyType === "IP") {
|
|
82
|
-
return () =>
|
|
82
|
+
return (request) => request.sourceIp;
|
|
83
83
|
}
|
|
84
84
|
if (keyType === "CONSTANT") {
|
|
85
85
|
if (statement.ScopeDownStatement === undefined) {
|
|
@@ -771,6 +771,12 @@ was created, exactly as `runAs` behaves. The header is stripped before the reque
|
|
|
771
771
|
simulated service. A Lambda handler echoing `event.headers` sees none of the simulator's control
|
|
772
772
|
metadata.
|
|
773
773
|
|
|
774
|
+
`x-sim-aws-source-ip` is a control header of the same kind. It names the client address a request
|
|
775
|
+
comes from (one IPv4 or IPv6 address), and a request naming none comes from `127.0.0.1`. A web ACL
|
|
776
|
+
reads it (see
|
|
777
|
+
[where a request comes from](https://yulinsim.dev/services/wafv2/#where-a-request-comes-from)). IAM
|
|
778
|
+
policy evaluation does not read it.
|
|
779
|
+
|
|
774
780
|
```typescript sim-iam-served-request-caller
|
|
775
781
|
/**
|
|
776
782
|
* Naming the caller of an HTTP request into simulated AWS.
|
|
@@ -282,10 +282,11 @@ lets evaluation continue, the way it does for every other statement kind.
|
|
|
282
282
|
window is measured against [simulated time](https://yulinsim.dev/time/), so `advanceBy` past it drops
|
|
283
283
|
what the rule counted.
|
|
284
284
|
|
|
285
|
-
`AggregateKeyType` is `IP` or `CONSTANT`. `IP` counts each client address on its own.
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
285
|
+
`AggregateKeyType` is `IP` or `CONSTANT`. `IP` counts each client address on its own. A request
|
|
286
|
+
comes from `127.0.0.1` unless it names another address (see
|
|
287
|
+
[Where a request comes from](#where-a-request-comes-from)). A test that names none has one client for
|
|
288
|
+
the whole simulation. That is the case a rate limiting test is usually written about (one client,
|
|
289
|
+
sending until the rule trips). `CONSTANT` counts every request the statement sees
|
|
289
290
|
together, and AWS requires a `ScopeDownStatement` alongside it to say which requests those are.
|
|
290
291
|
|
|
291
292
|
A `ScopeDownStatement` narrows what the rule counts. Every statement kind in
|
|
@@ -463,17 +464,103 @@ are.
|
|
|
463
464
|
|
|
464
465
|
### Groups that read the caller
|
|
465
466
|
|
|
466
|
-
The IP reputation, anonymous IP and Bot Control groups decide by who sent a request
|
|
467
|
-
|
|
468
|
-
labels the request, takes the rule's documented
|
|
469
|
-
read the request do.
|
|
467
|
+
The IP reputation, anonymous IP and Bot Control groups decide by who sent a request. AWS publishes
|
|
468
|
+
their rule names, actions and labels, and none of the address lists or bot signatures behind them.
|
|
469
|
+
Each of their rules is declared-only. The group labels the request, takes the rule's documented
|
|
470
|
+
action and honours overrides, as the groups that read the request do.
|
|
470
471
|
|
|
471
472
|
`AWSManagedRulesAmazonIpReputationList` holds `AWSManagedIPReputationList` and
|
|
472
473
|
`AWSManagedReconnaissanceList`, which block, and `AWSManagedIPDDoSList`, which counts as AWS ships
|
|
473
474
|
it. `AWSManagedRulesAnonymousIpList` holds `AnonymousIPList` (VPNs, proxies and Tor) and
|
|
474
475
|
`HostingProviderIPList` (hosting and cloud providers other than AWS), which both block. A test
|
|
475
|
-
declares
|
|
476
|
-
|
|
476
|
+
declares which addresses those five rules flag, and sends requests from them.
|
|
477
|
+
|
|
478
|
+
#### Where a request comes from
|
|
479
|
+
|
|
480
|
+
A request reaches this simulation in process or over loopback, and comes from `127.0.0.1`. The
|
|
481
|
+
`x-sim-aws-source-ip` header names another client address (one IPv4 or IPv6 address). It works
|
|
482
|
+
through `SimAwsHttp` and `serveSimAws` alike, and it is stripped before the request reaches the
|
|
483
|
+
simulated service, as `x-sim-aws-caller` is. A value that is no address is refused with a 400 and
|
|
484
|
+
`InvalidRequestCaller`. `evaluateRequest` takes the address as `sourceIp`.
|
|
485
|
+
|
|
486
|
+
The address reaches the web ACL in front of a Cognito user pool or a REST API stage. The IP
|
|
487
|
+
reputation and anonymous IP rules read it, and so does a rate limit aggregating on `IP`.
|
|
488
|
+
|
|
489
|
+
`managedRules().onAddress` says which of those rules flag one address or a CIDR range. Only the
|
|
490
|
+
rules of the IP reputation and anonymous IP groups can be declared by address, and a rule of any
|
|
491
|
+
other group is refused. Declaring the same range again replaces it. An address inside two declared
|
|
492
|
+
ranges is flagged by the rules of both, and a match declared for the request's path with
|
|
493
|
+
`onRequest` applies beside them.
|
|
494
|
+
|
|
495
|
+
```typescript sim-wafv2-anonymous-ip-list
|
|
496
|
+
/**
|
|
497
|
+
* Blocking sign-ups from a Tor exit range with the anonymous IP list.
|
|
498
|
+
*/
|
|
499
|
+
|
|
500
|
+
import { CreateWebACLCommand } from "@aws-sdk/client-wafv2";
|
|
501
|
+
|
|
502
|
+
import { SimAws } from "@kensio/yulin";
|
|
503
|
+
|
|
504
|
+
const waf = new SimAws().wafV2();
|
|
505
|
+
|
|
506
|
+
const visibility = {
|
|
507
|
+
SampledRequestsEnabled: false,
|
|
508
|
+
CloudWatchMetricsEnabled: false,
|
|
509
|
+
MetricName: "pool",
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const created = await waf.createWebAcl(
|
|
513
|
+
new CreateWebACLCommand({
|
|
514
|
+
Name: "pool-acl",
|
|
515
|
+
Scope: "REGIONAL",
|
|
516
|
+
DefaultAction: { Allow: {} },
|
|
517
|
+
VisibilityConfig: visibility,
|
|
518
|
+
Rules: [
|
|
519
|
+
{
|
|
520
|
+
Name: "anonymous-ip",
|
|
521
|
+
Priority: 0,
|
|
522
|
+
OverrideAction: { None: {} },
|
|
523
|
+
Statement: {
|
|
524
|
+
ManagedRuleGroupStatement: {
|
|
525
|
+
VendorName: "AWS",
|
|
526
|
+
Name: "AWSManagedRulesAnonymousIpList",
|
|
527
|
+
},
|
|
528
|
+
},
|
|
529
|
+
VisibilityConfig: { ...visibility, MetricName: "anonymous-ip" },
|
|
530
|
+
},
|
|
531
|
+
],
|
|
532
|
+
}),
|
|
533
|
+
);
|
|
534
|
+
|
|
535
|
+
waf.managedRules().onAddress("185.220.101.0/24", {
|
|
536
|
+
matches: ["AnonymousIPList"],
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
const signUp = (sourceIp: string): string =>
|
|
540
|
+
waf.evaluateRequest({
|
|
541
|
+
webAclArn: created.Summary!.ARN,
|
|
542
|
+
request: new Request("https://myapp-login.example.com/signup", {
|
|
543
|
+
headers: { "user-agent": "Mozilla/5.0" },
|
|
544
|
+
}),
|
|
545
|
+
sourceIp,
|
|
546
|
+
}).action;
|
|
547
|
+
|
|
548
|
+
// "BLOCK"
|
|
549
|
+
console.log(signUp("185.220.101.7"));
|
|
550
|
+
|
|
551
|
+
// "ALLOW"
|
|
552
|
+
console.log(signUp("203.0.113.9"));
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
A request over HTTP names its address in the header instead:
|
|
556
|
+
|
|
557
|
+
```text
|
|
558
|
+
POST /signup HTTP/1.1
|
|
559
|
+
Host: myapp-login.auth.eu-west-2.amazoncognito.com
|
|
560
|
+
x-sim-aws-source-ip: 185.220.101.7
|
|
561
|
+
```
|
|
562
|
+
|
|
563
|
+
The other kinds of statement that read the address are still held (see [Held input](#held-input)).
|
|
477
564
|
|
|
478
565
|
Bot Control labels a request by the bot that sent it, and a verified bot is labelled and matches no
|
|
479
566
|
rule. A test declares the bot.
|
|
@@ -865,6 +952,9 @@ and records no message.
|
|
|
865
952
|
|
|
866
953
|
The pages are usually the point. `/signup`, `/confirm` and `/forgotPassword` are the ones that
|
|
867
954
|
create an account or send an email, and a real web ACL on a user pool is usually written for them.
|
|
955
|
+
A request to them can name the client it comes from in the `x-sim-aws-source-ip` header, for the
|
|
956
|
+
anonymous IP list and the IP reputation list to decide on (see
|
|
957
|
+
[Where a request comes from](#where-a-request-comes-from)).
|
|
868
958
|
|
|
869
959
|
```typescript sim-wafv2-cognito-user-pool
|
|
870
960
|
/**
|
|
@@ -1537,9 +1627,10 @@ where AWS blocks every address outside the list and the simulation blocks none.
|
|
|
1537
1627
|
|
|
1538
1628
|
A rule is held for any of these statement kinds:
|
|
1539
1629
|
|
|
1540
|
-
- `IPSetReferenceStatement`, `GeoMatchStatement` and `AsnMatchStatement`.
|
|
1541
|
-
|
|
1542
|
-
|
|
1630
|
+
- `IPSetReferenceStatement`, `GeoMatchStatement` and `AsnMatchStatement`. A request has a client
|
|
1631
|
+
address (see [Where a request comes from](#where-a-request-comes-from)), and these statements are
|
|
1632
|
+
not yet written to read it. A country or an ASN would also need an address database the simulation
|
|
1633
|
+
does not have.
|
|
1543
1634
|
- `SqliMatchStatement` and `XssMatchStatement`. AWS publishes no description of the detection they
|
|
1544
1635
|
run.
|
|
1545
1636
|
- `RuleGroupReferenceStatement`. A rule group of your own is a resource in its own right, and none
|
|
@@ -1551,7 +1642,7 @@ A rule is held for any of these statement kinds:
|
|
|
1551
1642
|
|
|
1552
1643
|
A `RateBasedStatement` is evaluated (see [Rate limiting](#rate-limiting)). A rule using two of its
|
|
1553
1644
|
aggregation key types is held. `FORWARDED_IP` and `ForwardedIPConfig` read the address from a
|
|
1554
|
-
forwarding header,
|
|
1645
|
+
forwarding header, and are held along with the IP set statement. `CUSTOM_KEYS` and
|
|
1555
1646
|
`CustomKeys` aggregate on headers, cookies and query arguments, and are feasible and not part of
|
|
1556
1647
|
this yet. `GetRateBasedStatementManagedKeys` is not simulated.
|
|
1557
1648
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kensio/yulin",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"description": "AWS system behaviour simulation for isolated unit testing",
|
|
5
5
|
"repository": "https://github.com/KensioSoftware/yulin",
|
|
6
6
|
"homepage": "https://yulinsim.dev/",
|
|
@@ -298,9 +298,9 @@
|
|
|
298
298
|
"execa": "^10.0.0",
|
|
299
299
|
"fta-cli": "^3.0.0",
|
|
300
300
|
"hyparquet": "^1.29.2",
|
|
301
|
-
"hyparquet-writer": "^0.
|
|
301
|
+
"hyparquet-writer": "^0.18.0",
|
|
302
302
|
"node-sql-parser": "^5.4.0",
|
|
303
|
-
"oxfmt": "^0.
|
|
303
|
+
"oxfmt": "^0.75.0",
|
|
304
304
|
"oxlint": "^1.77.0",
|
|
305
305
|
"oxlint-tsgolint": "^7.0.2001",
|
|
306
306
|
"semantic-release": "25.0.9",
|