@kensio/yulin 1.21.23 → 1.21.24
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/service/wafv2/association/sim-aws-waf-protected-resources.js +4 -0
- package/dist/service/wafv2/association/sim-waf-associations.d.ts +6 -0
- package/dist/service/wafv2/association/sim-waf-associations.js +17 -0
- package/dist/service/wafv2/association/sim-waf-protected-resource.d.ts +13 -8
- package/dist/service/wafv2/association/sim-waf-protected-resource.js +23 -23
- package/dist/service/wafv2/association/sim-waf-protected-resources.d.ts +11 -6
- package/dist/service/wafv2/association/sim-waf-protected-resources.js +10 -7
- package/dist/service/wafv2/association/sim-waf-unsimulated-resource.d.ts +33 -0
- package/dist/service/wafv2/association/sim-waf-unsimulated-resource.js +66 -0
- package/dist/service/wafv2/cfn/association/sim-cfn-waf-association-creator.js +5 -0
- package/dist/service/wafv2/cfn/ip-set/sim-cfn-waf-ip-set-config.js +1 -0
- package/dist/service/wafv2/cfn/ip-set/sim-cfn-waf-ip-set-creator.js +2 -0
- package/dist/service/wafv2/cfn/regex-pattern-set/sim-cfn-waf-regex-set-config.js +1 -0
- package/dist/service/wafv2/cfn/regex-pattern-set/sim-cfn-waf-regex-set-creator.js +2 -0
- package/dist/service/wafv2/cfn/sim-cfn-waf-resource-config.d.ts +4 -0
- package/dist/service/wafv2/cfn/sim-cfn-waf-resource-config.js +10 -0
- package/dist/service/wafv2/cfn/sim-cfn-waf-unsimulated.d.ts +10 -0
- package/dist/service/wafv2/cfn/sim-cfn-waf-unsimulated.js +12 -0
- package/dist/service/wafv2/cfn/web-acl/sim-cfn-waf-web-acl-config.d.ts +1 -13
- package/dist/service/wafv2/cfn/web-acl/sim-cfn-waf-web-acl-config.js +8 -21
- package/dist/service/wafv2/cfn/web-acl/sim-cfn-waf-web-acl-creator.d.ts +4 -9
- package/dist/service/wafv2/cfn/web-acl/sim-cfn-waf-web-acl-creator.js +7 -15
- package/dist/service/wafv2/command/association/sim-wafv2-association-commands.js +2 -1
- package/dist/service/wafv2/command/association/sim-wafv2-association-input.d.ts +10 -2
- package/dist/service/wafv2/command/association/sim-wafv2-association-input.js +25 -13
- package/dist/service/wafv2/command/ip-set/sim-wafv2-ip-set-commands.d.ts +3 -3
- package/dist/service/wafv2/command/ip-set/sim-wafv2-ip-set-commands.js +5 -5
- package/dist/service/wafv2/command/managed-rule-group/sim-wafv2-managed-rule-group-commands.js +4 -4
- package/dist/service/wafv2/command/regex-pattern-set/sim-wafv2-regex-pattern-set-commands.js +2 -2
- package/dist/service/wafv2/command/sim-wafv2-input.d.ts +0 -7
- package/dist/service/wafv2/command/sim-wafv2-input.js +1 -13
- package/dist/service/wafv2/command/web-acl/sim-waf-web-acl-output.js +1 -0
- package/dist/service/wafv2/command/web-acl/sim-wafv2-unsimulated-web-acl-input.d.ts +9 -12
- package/dist/service/wafv2/command/web-acl/sim-wafv2-unsimulated-web-acl-input.js +27 -28
- package/dist/service/wafv2/command/web-acl/sim-wafv2-web-acl-commands.d.ts +3 -3
- package/dist/service/wafv2/command/web-acl/sim-wafv2-web-acl-commands.js +6 -6
- package/dist/service/wafv2/command/web-acl/web-acl.command.d.ts +6 -0
- package/dist/service/wafv2/index.d.ts +3 -0
- package/dist/service/wafv2/index.js +1 -0
- package/dist/service/wafv2/managed/group/sim-waf-bot-control.d.ts +27 -0
- package/dist/service/wafv2/managed/group/sim-waf-bot-control.js +68 -0
- package/dist/service/wafv2/managed/group/sim-waf-ip-reputation.d.ts +18 -0
- package/dist/service/wafv2/managed/group/sim-waf-ip-reputation.js +51 -0
- package/dist/service/wafv2/managed/sim-waf-bot-declaration.d.ts +35 -0
- package/dist/service/wafv2/managed/sim-waf-bot-declaration.js +42 -0
- package/dist/service/wafv2/managed/sim-waf-managed-declaration.d.ts +35 -0
- package/dist/service/wafv2/managed/sim-waf-managed-declaration.js +30 -0
- package/dist/service/wafv2/managed/sim-waf-managed-group-configs.d.ts +11 -0
- package/dist/service/wafv2/managed/sim-waf-managed-group-configs.js +42 -0
- package/dist/service/wafv2/managed/sim-waf-managed-group-input.d.ts +5 -4
- package/dist/service/wafv2/managed/sim-waf-managed-group-input.js +7 -9
- package/dist/service/wafv2/managed/sim-waf-managed-group-statement.js +16 -14
- package/dist/service/wafv2/managed/sim-waf-managed-group.type.d.ts +2 -2
- package/dist/service/wafv2/managed/sim-waf-managed-rule-actions.d.ts +30 -0
- package/dist/service/wafv2/managed/sim-waf-managed-rule-actions.js +26 -0
- package/dist/service/wafv2/managed/sim-waf-managed-rule-groups.d.ts +5 -3
- package/dist/service/wafv2/managed/sim-waf-managed-rule-groups.js +10 -3
- package/dist/service/wafv2/managed/sim-waf-managed-rule.type.d.ts +11 -2
- package/dist/service/wafv2/managed/sim-waf-managed-rules.d.ts +14 -9
- package/dist/service/wafv2/managed/sim-waf-managed-rules.js +15 -5
- package/dist/service/wafv2/resource/sim-waf-resource.d.ts +10 -0
- package/dist/service/wafv2/resource/sim-waf-resource.js +12 -0
- package/dist/service/wafv2/resource/sim-waf-unsimulated-part.d.ts +24 -0
- package/dist/service/wafv2/resource/sim-waf-unsimulated-part.js +8 -0
- package/dist/service/wafv2/sim-wafv2.d.ts +12 -12
- package/dist/service/wafv2/sim-wafv2.fixture.d.ts +19 -1
- package/dist/service/wafv2/sim-wafv2.fixture.js +29 -0
- package/dist/service/wafv2/sim-wafv2.js +25 -23
- package/dist/service/wafv2/statement/sim-waf-field-to-match.type.d.ts +2 -2
- package/dist/service/wafv2/statement/sim-waf-rule-refusals.d.ts +6 -4
- package/dist/service/wafv2/statement/sim-waf-rule-refusals.js +7 -6
- package/dist/service/wafv2/statement/sim-waf-statement.d.ts +3 -3
- package/dist/service/wafv2/statement/sim-waf-statement.js +3 -3
- package/dist/service/wafv2/statement/sim-waf-statement.type.d.ts +3 -3
- package/dist/service/wafv2/statement/sim-waf-unsimulated-statement.js +1 -1
- package/dist/service/wafv2/web-acl/sim-waf-association-config.d.ts +14 -7
- package/dist/service/wafv2/web-acl/sim-waf-association-config.js +17 -27
- package/dist/service/wafv2/web-acl/sim-waf-rule.d.ts +15 -3
- package/dist/service/wafv2/web-acl/sim-waf-rule.js +46 -10
- package/dist/service/wafv2/web-acl/sim-waf-statement-capacity.d.ts +4 -4
- package/dist/service/wafv2/web-acl/sim-waf-statement-capacity.js +4 -4
- package/dist/service/wafv2/web-acl/sim-waf-unsimulated-body-limit.d.ts +6 -0
- package/dist/service/wafv2/web-acl/sim-waf-unsimulated-body-limit.js +23 -0
- package/dist/service/wafv2/web-acl/sim-waf-web-acl-capacity.js +2 -2
- package/dist/service/wafv2/web-acl/sim-waf-web-acl-configuration.d.ts +5 -0
- package/dist/service/wafv2/web-acl/sim-waf-web-acl.d.ts +11 -2
- package/dist/service/wafv2/web-acl/sim-waf-web-acl.js +20 -1
- package/docs/services/wafv2/README.md +243 -51
- package/package.json +2 -2
- package/dist/service/wafv2/cfn/web-acl/sim-cfn-waf-evaluatable-rules.d.ts +0 -15
- package/dist/service/wafv2/cfn/web-acl/sim-cfn-waf-evaluatable-rules.js +0 -21
- package/dist/service/wafv2/web-acl/sim-waf-unevaluatable-rules.d.ts +0 -25
- package/dist/service/wafv2/web-acl/sim-waf-unevaluatable-rules.js +0 -33
|
@@ -59,8 +59,8 @@ function nestedCapacity(statement) {
|
|
|
59
59
|
*
|
|
60
60
|
* A group is fixed at the capacity its owner gave it, and a scope-down
|
|
61
61
|
* statement is charged on top. A group this simulation does not carry
|
|
62
|
-
* contributes nothing,
|
|
63
|
-
*
|
|
62
|
+
* contributes nothing, so a web ACL holding one reports less than AWS would
|
|
63
|
+
* charge.
|
|
64
64
|
*/
|
|
65
65
|
function managedGroupCapacity(statement) {
|
|
66
66
|
const named = statement.ManagedRuleGroupStatement;
|
|
@@ -13,6 +13,11 @@ export interface SimWafWebAclConfiguration {
|
|
|
13
13
|
readonly visibilityConfig?: unknown;
|
|
14
14
|
readonly description?: string | undefined;
|
|
15
15
|
readonly associationConfig?: SimWafAssociationConfigInput | undefined;
|
|
16
|
+
/**
|
|
17
|
+
* The members held as written and not acted on, such as `CaptchaConfig`,
|
|
18
|
+
* keyed by the name the API gives them.
|
|
19
|
+
*/
|
|
20
|
+
readonly heldMembers?: Readonly<Record<string, unknown>> | undefined;
|
|
16
21
|
}
|
|
17
22
|
/**
|
|
18
23
|
* Read the action a request no rule claims is given.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type SimWafDecision } from "../evaluate/sim-waf-decision.js";
|
|
2
2
|
import type { SimWafInspectedRequest } from "../evaluate/sim-waf-inspected-request.js";
|
|
3
3
|
import { SimWafResource, type SimWafResourceProperties } from "../resource/sim-waf-resource.js";
|
|
4
|
-
import {
|
|
4
|
+
import type { SimWafUnsimulatedPart } from "../resource/sim-waf-unsimulated-part.js";
|
|
5
5
|
import type { SimWafWebAclRuleScope } from "./sim-waf-rule.type.js";
|
|
6
6
|
import { type SimWafWebAclConfiguration } from "./sim-waf-web-acl-configuration.js";
|
|
7
7
|
interface SimWafWebAclProperties extends SimWafResourceProperties, SimWafWebAclRuleScope {
|
|
@@ -33,6 +33,14 @@ export declare class SimWafWebAcl extends SimWafResource {
|
|
|
33
33
|
* What this web ACL's rules add up to in capacity units.
|
|
34
34
|
*/
|
|
35
35
|
get capacity(): number;
|
|
36
|
+
/**
|
|
37
|
+
* What this web ACL was written with and the simulation does not act on.
|
|
38
|
+
*
|
|
39
|
+
* A rule listed here claims no request, a member is held and does nothing,
|
|
40
|
+
* and a body inspection limit applies to no request. `GetWebACL` returns
|
|
41
|
+
* every one of them as written.
|
|
42
|
+
*/
|
|
43
|
+
get unsimulated(): readonly SimWafUnsimulatedPart[];
|
|
36
44
|
/**
|
|
37
45
|
* What this web ACL was last written with, as the API reports it.
|
|
38
46
|
*/
|
|
@@ -42,6 +50,7 @@ export declare class SimWafWebAcl extends SimWafResource {
|
|
|
42
50
|
*
|
|
43
51
|
* The rules are compiled before anything is replaced, so a web ACL that
|
|
44
52
|
* refuses an update keeps the rules it had rather than being left with none.
|
|
53
|
+
* The tags stay as they were created, since UpdateWebACL takes none.
|
|
45
54
|
*/
|
|
46
55
|
reconfigure(configuration: SimWafWebAclConfiguration, lockToken: string | undefined): void;
|
|
47
56
|
/**
|
|
@@ -51,7 +60,7 @@ export declare class SimWafWebAcl extends SimWafResource {
|
|
|
51
60
|
* `AssociationConfig` raises it per resource type, and a web ACL written
|
|
52
61
|
* without one reads the 16 KB default everything it protects has.
|
|
53
62
|
*/
|
|
54
|
-
bodyInspectionLimitBytes(resourceType:
|
|
63
|
+
bodyInspectionLimitBytes(resourceType: string | undefined): number;
|
|
55
64
|
/**
|
|
56
65
|
* Decide what happens to one request.
|
|
57
66
|
*/
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { simWafDecision, } from "../evaluate/sim-waf-decision.js";
|
|
2
2
|
import { simWafEvaluateRules } from "../evaluate/sim-waf-evaluate-rules.js";
|
|
3
|
+
import { unsimulatedSimWafWebAclMembers } from "../command/web-acl/sim-wafv2-unsimulated-web-acl-input.js";
|
|
3
4
|
import { SimWafResource, } from "../resource/sim-waf-resource.js";
|
|
4
|
-
import { SimWafBodyInspectionLimits
|
|
5
|
+
import { SimWafBodyInspectionLimits } from "./sim-waf-association-config.js";
|
|
5
6
|
import { compileSimWafWebAclRules } from "./sim-waf-rules.js";
|
|
6
7
|
import { simWafWebAclCapacity } from "./sim-waf-web-acl-capacity.js";
|
|
7
8
|
import { readSimWafDefaultAction, } from "./sim-waf-web-acl-configuration.js";
|
|
@@ -49,6 +50,23 @@ export class SimWafWebAcl extends SimWafResource {
|
|
|
49
50
|
get capacity() {
|
|
50
51
|
return this.#capacity;
|
|
51
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* What this web ACL was written with and the simulation does not act on.
|
|
55
|
+
*
|
|
56
|
+
* A rule listed here claims no request, a member is held and does nothing,
|
|
57
|
+
* and a body inspection limit applies to no request. `GetWebACL` returns
|
|
58
|
+
* every one of them as written.
|
|
59
|
+
*/
|
|
60
|
+
get unsimulated() {
|
|
61
|
+
return [
|
|
62
|
+
...this.#rules.flatMap(({ name, unsimulatedReason }) => unsimulatedReason === undefined
|
|
63
|
+
? []
|
|
64
|
+
: [{ part: `Rules.${name}`, reason: unsimulatedReason }]),
|
|
65
|
+
...unsimulatedSimWafWebAclMembers(this.#configuration.heldMembers ?? {}),
|
|
66
|
+
...this.#bodyInspectionLimits.unsimulated,
|
|
67
|
+
...super.unsimulated,
|
|
68
|
+
];
|
|
69
|
+
}
|
|
52
70
|
/**
|
|
53
71
|
* What this web ACL was last written with, as the API reports it.
|
|
54
72
|
*/
|
|
@@ -60,6 +78,7 @@ export class SimWafWebAcl extends SimWafResource {
|
|
|
60
78
|
*
|
|
61
79
|
* The rules are compiled before anything is replaced, so a web ACL that
|
|
62
80
|
* refuses an update keeps the rules it had rather than being left with none.
|
|
81
|
+
* The tags stay as they were created, since UpdateWebACL takes none.
|
|
63
82
|
*/
|
|
64
83
|
reconfigure(configuration, lockToken) {
|
|
65
84
|
const defaultAction = readSimWafDefaultAction(configuration);
|
|
@@ -302,13 +302,20 @@ both refused where the rule is written.
|
|
|
302
302
|
|
|
303
303
|
## The AWS managed rule groups
|
|
304
304
|
|
|
305
|
-
|
|
306
|
-
traffic can be tested against them.
|
|
305
|
+
Six of the AWS managed rule groups are simulated, so a stack that turns them on deploys and its
|
|
306
|
+
traffic can be tested against them. The first three read the request.
|
|
307
307
|
|
|
308
308
|
- `AWSManagedRulesCommonRuleSet`, the core rule set, 22 rules.
|
|
309
309
|
- `AWSManagedRulesKnownBadInputsRuleSet`, 11 rules.
|
|
310
310
|
- `AWSManagedRulesAdminProtectionRuleSet`, one rule.
|
|
311
311
|
|
|
312
|
+
The other three read the caller, and a test declares what they find (see
|
|
313
|
+
[Groups that read the caller](#groups-that-read-the-caller)).
|
|
314
|
+
|
|
315
|
+
- `AWSManagedRulesAmazonIpReputationList`, three rules.
|
|
316
|
+
- `AWSManagedRulesAnonymousIpList`, two rules.
|
|
317
|
+
- `AWSManagedRulesBotControlRuleSet` at the common inspection level, 19 rules.
|
|
318
|
+
|
|
312
319
|
A group evaluates its rules in the order AWS documents them, adds the documented
|
|
313
320
|
`awswaf:managed:aws:*` label to a request a rule claims, and blocks by that rule's action. The
|
|
314
321
|
labels are on the decision, and they are what says which rule inside a group claimed the request.
|
|
@@ -405,7 +412,8 @@ AWS rule it stands for, and `managedRules().rules()` reports the tier of every o
|
|
|
405
412
|
`ExploitablePaths_URIPATH`, `AdminProtection_URIPATH`, `JavaDeserializationRCE_*` and
|
|
406
413
|
`UserAgent_BadBots_HEADER`.
|
|
407
414
|
- **declared** detects nothing at all. The four `CrossSiteScripting_*` rules run AWS's own
|
|
408
|
-
detection, and AWS documents none of it.
|
|
415
|
+
detection, and AWS documents none of it. Every rule of the IP reputation, anonymous IP and Bot
|
|
416
|
+
Control groups is declared too.
|
|
409
417
|
|
|
410
418
|
The tiers under-detect against AWS and never over-detect. The usual reason to put WAF in a test is
|
|
411
419
|
to find out whether an application's own traffic still gets through with the core rule set on. A
|
|
@@ -447,10 +455,125 @@ A match names the rule, in the spelling `RuleActionOverrides` and `DescribeManag
|
|
|
447
455
|
(`CrossSiteScripting_QueryArguments`). A name no simulated group holds is refused where it was
|
|
448
456
|
written.
|
|
449
457
|
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
458
|
+
A rule naming any other group is held and claims no request (see [Held input](#held-input)), and
|
|
459
|
+
the reason it gives says which groups are simulated. The account takeover and account creation
|
|
460
|
+
groups, and Bot Control's targeted level, decide by behaviour across requests and by tokens a
|
|
461
|
+
browser carries. The SQL injection group is undocumented in the way the cross-site scripting rules
|
|
462
|
+
are.
|
|
463
|
+
|
|
464
|
+
### Groups that read the caller
|
|
465
|
+
|
|
466
|
+
The IP reputation, anonymous IP and Bot Control groups decide by who sent a request, and every
|
|
467
|
+
request in this simulation comes from `127.0.0.1`. Each of their rules is declared-only. The group
|
|
468
|
+
labels the request, takes the rule's documented action and honours overrides, as the groups that
|
|
469
|
+
read the request do.
|
|
470
|
+
|
|
471
|
+
`AWSManagedRulesAmazonIpReputationList` holds `AWSManagedIPReputationList` and
|
|
472
|
+
`AWSManagedReconnaissanceList`, which block, and `AWSManagedIPDDoSList`, which counts as AWS ships
|
|
473
|
+
it. `AWSManagedRulesAnonymousIpList` holds `AnonymousIPList` (VPNs, proxies and Tor) and
|
|
474
|
+
`HostingProviderIPList` (hosting and cloud providers other than AWS), which both block. A test
|
|
475
|
+
declares a match by rule name, as above, and says which requests come from a flagged address that
|
|
476
|
+
way.
|
|
477
|
+
|
|
478
|
+
Bot Control labels a request by the bot that sent it, and a verified bot is labelled and matches no
|
|
479
|
+
rule. A test declares the bot.
|
|
480
|
+
|
|
481
|
+
```typescript sim-wafv2-bot-control
|
|
482
|
+
/**
|
|
483
|
+
* Counting with Bot Control and blocking unverified bots by label.
|
|
484
|
+
*/
|
|
485
|
+
|
|
486
|
+
import { CreateWebACLCommand } from "@aws-sdk/client-wafv2";
|
|
487
|
+
|
|
488
|
+
import { SimAws } from "@kensio/yulin";
|
|
489
|
+
|
|
490
|
+
const waf = new SimAws().wafV2();
|
|
491
|
+
|
|
492
|
+
const visibility = {
|
|
493
|
+
SampledRequestsEnabled: false,
|
|
494
|
+
CloudWatchMetricsEnabled: false,
|
|
495
|
+
MetricName: "site",
|
|
496
|
+
};
|
|
497
|
+
|
|
498
|
+
const created = await waf.createWebAcl(
|
|
499
|
+
new CreateWebACLCommand({
|
|
500
|
+
Name: "site-acl",
|
|
501
|
+
Scope: "REGIONAL",
|
|
502
|
+
DefaultAction: { Allow: {} },
|
|
503
|
+
VisibilityConfig: visibility,
|
|
504
|
+
Rules: [
|
|
505
|
+
{
|
|
506
|
+
Name: "bot-control",
|
|
507
|
+
Priority: 0,
|
|
508
|
+
OverrideAction: { Count: {} },
|
|
509
|
+
Statement: {
|
|
510
|
+
ManagedRuleGroupStatement: {
|
|
511
|
+
VendorName: "AWS",
|
|
512
|
+
Name: "AWSManagedRulesBotControlRuleSet",
|
|
513
|
+
ManagedRuleGroupConfigs: [
|
|
514
|
+
{
|
|
515
|
+
AWSManagedRulesBotControlRuleSet: { InspectionLevel: "COMMON" },
|
|
516
|
+
},
|
|
517
|
+
],
|
|
518
|
+
},
|
|
519
|
+
},
|
|
520
|
+
VisibilityConfig: visibility,
|
|
521
|
+
},
|
|
522
|
+
{
|
|
523
|
+
Name: "block-scrapers",
|
|
524
|
+
Priority: 1,
|
|
525
|
+
Action: { Block: {} },
|
|
526
|
+
Statement: {
|
|
527
|
+
LabelMatchStatement: {
|
|
528
|
+
Scope: "LABEL",
|
|
529
|
+
Key: "awswaf:managed:aws:bot-control:bot:category:scraping_framework",
|
|
530
|
+
},
|
|
531
|
+
},
|
|
532
|
+
VisibilityConfig: visibility,
|
|
533
|
+
},
|
|
534
|
+
],
|
|
535
|
+
}),
|
|
536
|
+
);
|
|
537
|
+
|
|
538
|
+
waf.managedRules().onRequest("/search", {
|
|
539
|
+
bot: { category: "scraping_framework", name: "scrapy" },
|
|
540
|
+
});
|
|
541
|
+
waf.managedRules().onRequest("/", {
|
|
542
|
+
bot: { category: "search_engine", name: "googlebot", verified: true },
|
|
543
|
+
});
|
|
544
|
+
|
|
545
|
+
const webAclArn = created.Summary!.ARN;
|
|
546
|
+
|
|
547
|
+
const scraper = waf.evaluateRequest({
|
|
548
|
+
webAclArn,
|
|
549
|
+
request: new Request("https://example.test/search"),
|
|
550
|
+
});
|
|
551
|
+
const crawler = waf.evaluateRequest({
|
|
552
|
+
webAclArn,
|
|
553
|
+
request: new Request("https://example.test/"),
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// "BLOCK" "block-scrapers"
|
|
557
|
+
console.log(scraper.action, scraper.terminatingRuleName);
|
|
558
|
+
|
|
559
|
+
// "ALLOW" true
|
|
560
|
+
console.log(
|
|
561
|
+
crawler.action,
|
|
562
|
+
crawler.labels.includes("awswaf:managed:aws:bot-control:bot:verified"),
|
|
563
|
+
);
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
A `bot` declaration names the category in the spelling the `bot:category:` label uses
|
|
567
|
+
(`search_engine`, `http_library`, `scraping_framework` and the rest), and optionally the bot's
|
|
568
|
+
`name`, its `organization` and whether it is `verified`. An unverified bot matches its category's
|
|
569
|
+
rule (`CategorySearchEngine` and so on), which blocks by default. A verified bot matches no rule,
|
|
570
|
+
except a verified AI bot, which `CategoryAI` blocks as it does on AWS. Either way the group adds
|
|
571
|
+
`bot:category:`, `bot:name:` and `bot:organization:` labels where the declaration gives them, and
|
|
572
|
+
`bot:verified` or `bot:unverified`. The three `Signal*` rules are declared by rule name in
|
|
573
|
+
`matches`, and add their `signal:` label beside their own.
|
|
574
|
+
|
|
575
|
+
Bot Control runs at `COMMON` when `ManagedRuleGroupConfigs` names that level or when it is left out.
|
|
576
|
+
A rule naming the `TARGETED` level is held (see [Held input](#held-input)).
|
|
454
577
|
|
|
455
578
|
## Labels
|
|
456
579
|
|
|
@@ -704,9 +827,8 @@ await srv.close();
|
|
|
704
827
|
|
|
705
828
|
`DisassociateWebACL` takes the web ACL back off. `GetWebACLForResource` reports the web ACL one
|
|
706
829
|
stage carries, and `ListResourcesForWebACL` reports the stages one web ACL protects. That listing
|
|
707
|
-
takes a `ResourceType` of `API_GATEWAY`.
|
|
708
|
-
|
|
709
|
-
refused.
|
|
830
|
+
takes a `ResourceType` of `API_GATEWAY`. A listing that names no type lists
|
|
831
|
+
`APPLICATION_LOAD_BALANCER`, as real WAFv2 does.
|
|
710
832
|
|
|
711
833
|
Deleting the stage or the whole API takes the association with it. A stage deployed again under the
|
|
712
834
|
same name carries no web ACL. A web ACL that is still in front of a stage cannot be deleted, and
|
|
@@ -717,9 +839,14 @@ because a distribution takes its web ACL from the distribution and not from `Ass
|
|
|
717
839
|
ACL from another Region or another Account is refused, as it is on AWS.
|
|
718
840
|
|
|
719
841
|
An API Gateway HTTP API stage is refused. AWS WAF has no resource type for one, and an association
|
|
720
|
-
accepted here would let a test cover protection AWS never applies.
|
|
721
|
-
|
|
722
|
-
|
|
842
|
+
accepted here would let a test cover protection AWS never applies.
|
|
843
|
+
|
|
844
|
+
An Application Load Balancer, an AppSync API, an App Runner service and a Verified Access instance
|
|
845
|
+
are associated, and `GetWebACLForResource` and `ListResourcesForWebACL` report them. Simulated
|
|
846
|
+
requests reach a web ACL through a REST API stage or a user pool alone, so the association is held
|
|
847
|
+
for reading back and listed by `unsimulatedParts` (see [Held input](#held-input)). Yulin takes the
|
|
848
|
+
resource as named, without looking for it. An Amplify app is refused. AWS takes a `CLOUDFRONT` web ACL from `us-east-1`
|
|
849
|
+
for one, and a simulated WAFv2 associates the web ACLs of its own Region only.
|
|
723
850
|
|
|
724
851
|
## Protecting a Cognito user pool
|
|
725
852
|
|
|
@@ -854,9 +981,10 @@ The web ACL and the pool belong to one Account and Region. A `CLOUDFRONT` scope
|
|
|
854
981
|
because a distribution takes its web ACL from the distribution. A pool in another Account or another
|
|
855
982
|
Region is refused as well.
|
|
856
983
|
|
|
857
|
-
AWS also refuses a web ACL carrying `AWSManagedRulesATPRuleSet`, and it refuses the whole
|
|
858
|
-
over the one rule group.
|
|
859
|
-
|
|
984
|
+
AWS also refuses a web ACL carrying `AWSManagedRulesATPRuleSet`, and it refuses the whole
|
|
985
|
+
association over the one rule group. `AssociateWebACL` refuses it here too. The web ACL itself is
|
|
986
|
+
created, holding the group without evaluating it, like every managed rule group outside the
|
|
987
|
+
[six that are simulated](#the-aws-managed-rule-groups).
|
|
860
988
|
|
|
861
989
|
### The request body is withheld at a hosted domain
|
|
862
990
|
|
|
@@ -967,7 +1095,7 @@ An update to a pattern set reaches the rules pointing at it. A reference resolve
|
|
|
967
1095
|
the rule is written and reads its expressions when a request arrives, as it does on AWS.
|
|
968
1096
|
|
|
969
1097
|
IP sets are created, read, updated, listed and deleted the same way. No rule reads one, for the
|
|
970
|
-
reason in [
|
|
1098
|
+
reason in [Held input](#held-input) below.
|
|
971
1099
|
|
|
972
1100
|
## Deploying web ACLs with CloudFormation
|
|
973
1101
|
|
|
@@ -1040,29 +1168,26 @@ strings where the SDK takes a list of `RegexString` objects. Both are read here
|
|
|
1040
1168
|
CloudFormation writes them.
|
|
1041
1169
|
|
|
1042
1170
|
Every rule is compiled while the stack deploys. A rule this simulator will not evaluate (see
|
|
1043
|
-
[
|
|
1044
|
-
|
|
1045
|
-
|
|
1171
|
+
[Held input](#held-input)) is held by the web ACL and claims no request. It
|
|
1172
|
+
is recorded on `stack.ignoredProperties`, under the logical ID that declared it, with the reason
|
|
1173
|
+
`unsimulatedParts` gives.
|
|
1046
1174
|
|
|
1047
1175
|
```typescript
|
|
1048
|
-
const [
|
|
1176
|
+
const [held] = stack.ignoredProperties;
|
|
1049
1177
|
|
|
1050
1178
|
// "OrdersAcl Rules.block-countries"
|
|
1051
|
-
console.log(`${
|
|
1179
|
+
console.log(`${held.logicalId} ${held.path}`);
|
|
1052
1180
|
|
|
1053
1181
|
// "Rule block-countries uses the statement kind GeoMatchStatement, which
|
|
1054
1182
|
// Yulin does not simulate: ..."
|
|
1055
|
-
console.log(
|
|
1183
|
+
console.log(held.reason);
|
|
1056
1184
|
```
|
|
1057
1185
|
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
test loses, and `stack.ignoredProperties` is where to read it. An SDK caller writing the same rule
|
|
1061
|
-
is refused outright, because a request that was answered and then quietly emptied is a worse answer
|
|
1062
|
-
than a refusal.
|
|
1186
|
+
Requests the held rule would have blocked are served by whatever the web ACL is in front of. That
|
|
1187
|
+
is the size of what a test loses, and `stack.ignoredProperties` is where to read it.
|
|
1063
1188
|
|
|
1064
1189
|
The same goes for a web ACL member with no behaviour behind it, such as `CaptchaConfig`. The web ACL
|
|
1065
|
-
|
|
1190
|
+
holds it and the member is recorded.
|
|
1066
1191
|
|
|
1067
1192
|
A web ACL nothing coherent could be deployed from still fails the stack. A `Scope` outside
|
|
1068
1193
|
`REGIONAL` and `CLOUDFRONT`, a `Rules` written as an object, a `Name` written as a number. The
|
|
@@ -1080,8 +1205,8 @@ cover where the tail comes from.
|
|
|
1080
1205
|
covers an API Gateway REST API stage and a Cognito user pool. It goes through `AssociateWebACL` and
|
|
1081
1206
|
inherits that command's answers. An ARN naming an HTTP API stage fails the deployment, because AWS
|
|
1082
1207
|
WAF protects no HTTP API and neither does real CloudFormation. An ARN naming a load balancer or an
|
|
1083
|
-
AppSync API
|
|
1084
|
-
|
|
1208
|
+
AppSync API is associated, and `ResourceArn` is recorded on `stack.ignoredProperties` as an
|
|
1209
|
+
association held for reading back. An ARN naming an Amplify app skips the association.
|
|
1085
1210
|
|
|
1086
1211
|
An association naming a web ACL from outside this simulation is skipped too, which covers a template
|
|
1087
1212
|
naming one from a real account and one whose web ACL is in another Region. The stage or the pool
|
|
@@ -1143,7 +1268,8 @@ composite primary identifier of name, ID and scope. An association's physical ID
|
|
|
1143
1268
|
and the web ACL ARN joined by a pipe, and it publishes no attributes.
|
|
1144
1269
|
|
|
1145
1270
|
`AWS::WAFv2::RuleGroup` and `AWS::WAFv2::LoggingConfiguration` are recorded as unsupported and
|
|
1146
|
-
stepped over. A rule naming a rule group is
|
|
1271
|
+
stepped over. A rule naming a rule group is held and not evaluated anyway, and there is no log here
|
|
1272
|
+
to write to.
|
|
1147
1273
|
|
|
1148
1274
|
## Scopes
|
|
1149
1275
|
|
|
@@ -1344,17 +1470,72 @@ const scoped = simSdk.simAws.accountRegionScope(
|
|
|
1344
1470
|
console.log(scoped.wafV2().allWebAcls("REGIONAL")[0]?.name);
|
|
1345
1471
|
```
|
|
1346
1472
|
|
|
1347
|
-
##
|
|
1473
|
+
## Held input
|
|
1474
|
+
|
|
1475
|
+
A write WAFv2 itself would accept succeeds, whatever it carries, and `GetWebACL` returns it as
|
|
1476
|
+
written. A rule using something outside the simulation is held, and it claims no request. `unsimulatedParts` lists each part the simulation leaves out, with the reason.
|
|
1477
|
+
|
|
1478
|
+
```typescript sim-wafv2-unsimulated
|
|
1479
|
+
/**
|
|
1480
|
+
* Writing a web ACL with a rule Yulin does not evaluate.
|
|
1481
|
+
*/
|
|
1482
|
+
|
|
1483
|
+
import { CreateWebACLCommand } from "@aws-sdk/client-wafv2";
|
|
1484
|
+
|
|
1485
|
+
import { SimAws } from "@kensio/yulin";
|
|
1486
|
+
|
|
1487
|
+
const waf = new SimAws().wafV2();
|
|
1488
|
+
|
|
1489
|
+
const visibility = {
|
|
1490
|
+
SampledRequestsEnabled: false,
|
|
1491
|
+
CloudWatchMetricsEnabled: false,
|
|
1492
|
+
MetricName: "site",
|
|
1493
|
+
};
|
|
1494
|
+
|
|
1495
|
+
const created = await waf.createWebAcl(
|
|
1496
|
+
new CreateWebACLCommand({
|
|
1497
|
+
Name: "site-acl",
|
|
1498
|
+
Scope: "REGIONAL",
|
|
1499
|
+
DefaultAction: { Allow: {} },
|
|
1500
|
+
VisibilityConfig: visibility,
|
|
1501
|
+
Rules: [
|
|
1502
|
+
{
|
|
1503
|
+
Name: "bot-control",
|
|
1504
|
+
Priority: 0,
|
|
1505
|
+
OverrideAction: { Count: {} },
|
|
1506
|
+
Statement: {
|
|
1507
|
+
ManagedRuleGroupStatement: {
|
|
1508
|
+
VendorName: "AWS",
|
|
1509
|
+
Name: "AWSManagedRulesBotControlRuleSet",
|
|
1510
|
+
ManagedRuleGroupConfigs: [
|
|
1511
|
+
{
|
|
1512
|
+
AWSManagedRulesBotControlRuleSet: { InspectionLevel: "COMMON" },
|
|
1513
|
+
},
|
|
1514
|
+
],
|
|
1515
|
+
},
|
|
1516
|
+
},
|
|
1517
|
+
VisibilityConfig: visibility,
|
|
1518
|
+
},
|
|
1519
|
+
],
|
|
1520
|
+
}),
|
|
1521
|
+
);
|
|
1348
1522
|
|
|
1349
|
-
|
|
1350
|
-
what in it was refused. A web ACL that accepted such a rule would allow a request AWS blocks, and a
|
|
1351
|
-
silent hole in a security layer is worse than a missing one.
|
|
1523
|
+
const [part] = waf.unsimulatedParts(created.Summary!.ARN);
|
|
1352
1524
|
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1525
|
+
// "Rules.bot-control"
|
|
1526
|
+
console.log(part?.part);
|
|
1527
|
+
|
|
1528
|
+
// "Rule bot-control uses the rule group member ManagedRuleGroupConfigs,
|
|
1529
|
+
// which Yulin does not simulate: ..."
|
|
1530
|
+
console.log(part?.reason);
|
|
1531
|
+
```
|
|
1356
1532
|
|
|
1357
|
-
|
|
1533
|
+
The rule keeps its `Action` or `OverrideAction` as written and never matches. That is what AWS does
|
|
1534
|
+
with a request the rule would not have claimed. A request it would have claimed goes on to the next
|
|
1535
|
+
rule. An allow list (a `NotStatement` around an `IPSetReferenceStatement`) is the sharpest case,
|
|
1536
|
+
where AWS blocks every address outside the list and the simulation blocks none.
|
|
1537
|
+
|
|
1538
|
+
A rule is held for any of these statement kinds:
|
|
1358
1539
|
|
|
1359
1540
|
- `IPSetReferenceStatement`, `GeoMatchStatement` and `AsnMatchStatement`. Every request in this
|
|
1360
1541
|
simulation reports a source address of `127.0.0.1`, and a rule on where a request came from would
|
|
@@ -1362,23 +1543,34 @@ These statement kinds are refused:
|
|
|
1362
1543
|
- `SqliMatchStatement` and `XssMatchStatement`. AWS publishes no description of the detection they
|
|
1363
1544
|
run.
|
|
1364
1545
|
- `RuleGroupReferenceStatement`. A rule group of your own is a resource in its own right, and none
|
|
1365
|
-
is simulated. The
|
|
1546
|
+
is simulated. The six simulated AWS managed rule groups are named in a statement rather than
|
|
1366
1547
|
created.
|
|
1548
|
+
- `ManagedRuleGroupStatement` naming a group outside the six, or carrying `Version` or
|
|
1549
|
+
`ExcludedRules`. `ManagedRuleGroupConfigs` holds the rule too, unless it runs Bot Control at
|
|
1550
|
+
`COMMON`.
|
|
1367
1551
|
|
|
1368
|
-
A `RateBasedStatement` is evaluated (see [Rate limiting](#rate-limiting)).
|
|
1369
|
-
key types
|
|
1370
|
-
header, which needs the source address variety an IP set is waiting on. `CUSTOM_KEYS` and
|
|
1552
|
+
A `RateBasedStatement` is evaluated (see [Rate limiting](#rate-limiting)). A rule using two of its
|
|
1553
|
+
aggregation key types is held. `FORWARDED_IP` and `ForwardedIPConfig` read the address from a
|
|
1554
|
+
forwarding header, which needs the source address variety an IP set is waiting on. `CUSTOM_KEYS` and
|
|
1371
1555
|
`CustomKeys` aggregate on headers, cookies and query arguments, and are feasible and not part of
|
|
1372
1556
|
this yet. `GetRateBasedStatementManagedKeys` is not simulated.
|
|
1373
1557
|
|
|
1374
|
-
`JsonBody`, `HeaderOrder`, `UriFragment`, `JA3Fingerprint` and
|
|
1375
|
-
fields to match
|
|
1376
|
-
`
|
|
1558
|
+
A rule is held for the `JsonBody`, `HeaderOrder`, `UriFragment`, `JA3Fingerprint` and
|
|
1559
|
+
`JA4Fingerprint` fields to match, and for the `Captcha` and `Challenge` actions and the rule-level
|
|
1560
|
+
`CaptchaConfig` and `ChallengeConfig`, because a browser has to answer them.
|
|
1561
|
+
|
|
1562
|
+
The web ACL holds these members as written, and they have no effect on a request. `CaptchaConfig`,
|
|
1563
|
+
`ChallengeConfig` and `TokenDomains` configure the browser-answered actions. `DataProtectionConfig` shapes web ACL
|
|
1564
|
+
logs, which Yulin leaves out. `OnSourceDDoSProtectionConfig` responds to
|
|
1565
|
+
traffic volume, and `ApplicationConfig` configures the WAF-hosted sign-in pages. An
|
|
1566
|
+
`AssociationConfig` limit for a resource type no simulated request passes through is held too.
|
|
1567
|
+
|
|
1568
|
+
Tags on a web ACL, an IP set or a regex pattern set are held, and `unsimulatedParts` lists them.
|
|
1569
|
+
`ListTagsForResource` is the operation that would read them back, and Yulin leaves it out. Logging, sampled requests and CloudWatch metrics for a
|
|
1570
|
+
web ACL are not simulated.
|
|
1377
1571
|
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
same reason, each naming what it would have configured. An `AssociationConfig` naming a resource
|
|
1381
|
-
type this simulation has no association for is refused too.
|
|
1572
|
+
`DescribeManagedRuleGroup` refuses a group outside the six, and a `VersionName`. Its only possible
|
|
1573
|
+
answer would be invented rules.
|
|
1382
1574
|
|
|
1383
1575
|
## Supported operations
|
|
1384
1576
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kensio/yulin",
|
|
3
|
-
"version": "1.21.
|
|
3
|
+
"version": "1.21.24",
|
|
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/",
|
|
@@ -299,7 +299,7 @@
|
|
|
299
299
|
"hyparquet": "^1.29.2",
|
|
300
300
|
"hyparquet-writer": "^0.16.9",
|
|
301
301
|
"node-sql-parser": "^5.4.0",
|
|
302
|
-
"oxfmt": "^0.
|
|
302
|
+
"oxfmt": "^0.70.0",
|
|
303
303
|
"oxlint": "^1.77.0",
|
|
304
304
|
"oxlint-tsgolint": "^7.0.2001",
|
|
305
305
|
"semantic-release": "25.0.9",
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import type { SimCfnResource } from "../../../cloudformation/resource/sim-cfn-resource.js";
|
|
2
|
-
import type { SimCreateWebAclCommandInput } from "../../command/web-acl/web-acl.command.js";
|
|
3
|
-
import type { SimWafV2 } from "../../sim-wafv2.js";
|
|
4
|
-
import type { SimWafRuleInput } from "../../web-acl/sim-waf-rule.type.js";
|
|
5
|
-
/**
|
|
6
|
-
* The rules of a template web ACL to deploy, which are the ones this
|
|
7
|
-
* simulation can evaluate.
|
|
8
|
-
*
|
|
9
|
-
* Each rule left out is recorded on the Resource under its own name, because a
|
|
10
|
-
* web ACL is a list of rules that all look alike from the outside and the name
|
|
11
|
-
* is the only thing that says which one went missing. The reason is the one
|
|
12
|
-
* `CreateWebACL` refuses the rule with, so a reader gets the same sentence
|
|
13
|
-
* wherever they meet it.
|
|
14
|
-
*/
|
|
15
|
-
export declare function simCfnWafEvaluatableRules(wafV2: SimWafV2, resource: SimCfnResource, input: SimCreateWebAclCommandInput): readonly SimWafRuleInput[] | undefined;
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The rules of a template web ACL to deploy, which are the ones this
|
|
3
|
-
* simulation can evaluate.
|
|
4
|
-
*
|
|
5
|
-
* Each rule left out is recorded on the Resource under its own name, because a
|
|
6
|
-
* web ACL is a list of rules that all look alike from the outside and the name
|
|
7
|
-
* is the only thing that says which one went missing. The reason is the one
|
|
8
|
-
* `CreateWebACL` refuses the rule with, so a reader gets the same sentence
|
|
9
|
-
* wherever they meet it.
|
|
10
|
-
*/
|
|
11
|
-
export function simCfnWafEvaluatableRules(wafV2, resource, input) {
|
|
12
|
-
const unevaluatable = wafV2.unevaluatableWebAclRules(input);
|
|
13
|
-
if (unevaluatable.length === 0) {
|
|
14
|
-
return input.Rules;
|
|
15
|
-
}
|
|
16
|
-
const dropped = new Set(unevaluatable.map(({ index }) => index));
|
|
17
|
-
for (const rule of unevaluatable) {
|
|
18
|
-
resource.ignoreProperty(`Rules.${rule.name}`, rule.reason);
|
|
19
|
-
}
|
|
20
|
-
return input.Rules?.filter((_, index) => !dropped.has(index));
|
|
21
|
-
}
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import type { SimWafRuleInput, SimWafRuleScope } from "./sim-waf-rule.type.js";
|
|
2
|
-
/**
|
|
3
|
-
* One rule of a web ACL this simulation cannot evaluate, and why.
|
|
4
|
-
*/
|
|
5
|
-
export interface SimWafUnevaluatableRule {
|
|
6
|
-
/** Where the rule sits in the list the web ACL was written with. */
|
|
7
|
-
readonly index: number;
|
|
8
|
-
/** The rule's name, as the reason and a record of it name it. */
|
|
9
|
-
readonly name: string;
|
|
10
|
-
/** What `CreateWebACL` refuses the rule with. */
|
|
11
|
-
readonly reason: string;
|
|
12
|
-
}
|
|
13
|
-
/**
|
|
14
|
-
* The rules of a web ACL this simulation cannot evaluate.
|
|
15
|
-
*
|
|
16
|
-
* Each rule is compiled on its own and the refusal is caught, so this answers
|
|
17
|
-
* with exactly what `CreateWebACL` would refuse and nothing else. A rule the
|
|
18
|
-
* simulation can evaluate is compiled twice, once here and once by the write
|
|
19
|
-
* that follows, and compiling is a pure reading of the rule.
|
|
20
|
-
*
|
|
21
|
-
* Only a refusal saying the simulation does not go that far is collected. Rule
|
|
22
|
-
* input WAFv2 itself will not take is left where it is, for the write to raise
|
|
23
|
-
* as it always has.
|
|
24
|
-
*/
|
|
25
|
-
export declare function unevaluatableSimWafRules(rules: readonly SimWafRuleInput[] | undefined, scope: SimWafRuleScope): readonly SimWafUnevaluatableRule[];
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { SimWafUnsimulatedInputException } from "../error/sim-wafv2.error.js";
|
|
2
|
-
import { SimWafRule } from "./sim-waf-rule.js";
|
|
3
|
-
/**
|
|
4
|
-
* The rules of a web ACL this simulation cannot evaluate.
|
|
5
|
-
*
|
|
6
|
-
* Each rule is compiled on its own and the refusal is caught, so this answers
|
|
7
|
-
* with exactly what `CreateWebACL` would refuse and nothing else. A rule the
|
|
8
|
-
* simulation can evaluate is compiled twice, once here and once by the write
|
|
9
|
-
* that follows, and compiling is a pure reading of the rule.
|
|
10
|
-
*
|
|
11
|
-
* Only a refusal saying the simulation does not go that far is collected. Rule
|
|
12
|
-
* input WAFv2 itself will not take is left where it is, for the write to raise
|
|
13
|
-
* as it always has.
|
|
14
|
-
*/
|
|
15
|
-
export function unevaluatableSimWafRules(rules, scope) {
|
|
16
|
-
return (rules ?? []).flatMap((rule, index) => {
|
|
17
|
-
const reason = unevaluatableReason(rule, scope);
|
|
18
|
-
return reason === undefined
|
|
19
|
-
? []
|
|
20
|
-
: [{ index, name: rule.Name ?? `at ${index}`, reason }];
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
function unevaluatableReason(rule, scope) {
|
|
24
|
-
try {
|
|
25
|
-
SimWafRule.compile(rule, scope);
|
|
26
|
-
}
|
|
27
|
-
catch (error) {
|
|
28
|
-
if (error instanceof SimWafUnsimulatedInputException) {
|
|
29
|
-
return error.message;
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|