@kensio/yulin 1.20.10 → 1.20.12
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/athena/engine/sim-athena-json-records.d.ts +23 -0
- package/dist/service/athena/engine/sim-athena-json-records.js +46 -0
- package/dist/service/athena/engine/sim-athena-object-codec.d.ts +13 -0
- package/dist/service/athena/engine/sim-athena-object-codec.js +40 -0
- package/dist/service/athena/engine/sim-athena-record-reader.js +26 -10
- package/dist/service/athena/engine/sim-athena-regexp-flags.d.ts +22 -0
- package/dist/service/athena/engine/sim-athena-regexp-flags.js +34 -0
- package/dist/service/athena/engine/sim-athena-regexp-shims.js +3 -1
- package/dist/service/athena/engine/sim-athena-table-objects.d.ts +1 -1
- package/dist/service/athena/engine/sim-athena-table-objects.js +4 -2
- package/dist/service/athena/engine/sim-athena-url-parts.d.ts +24 -0
- package/dist/service/athena/engine/sim-athena-url-parts.js +76 -0
- package/dist/service/athena/engine/sim-athena-url-shims.d.ts +3 -2
- package/dist/service/athena/engine/sim-athena-url-shims.js +57 -25
- package/dist/service/athena/sim-athena-engine.fixture.d.ts +2 -0
- package/dist/service/athena/sim-athena-engine.fixture.js +6 -0
- package/dist/service/athena/table/sim-athena-negated-columns.d.ts +17 -0
- package/dist/service/athena/table/sim-athena-negated-columns.js +80 -0
- package/dist/service/athena/table/sim-athena-partition-filters.d.ts +7 -4
- package/dist/service/athena/table/sim-athena-partition-filters.js +12 -5
- package/dist/service/wafv2/command/ip-set/sim-wafv2-ip-set-commands.js +4 -3
- package/dist/service/wafv2/command/regex-pattern-set/sim-wafv2-regex-pattern-set-commands.js +4 -3
- package/dist/service/wafv2/command/sim-wafv2-input.d.ts +15 -0
- package/dist/service/wafv2/command/sim-wafv2-input.js +66 -1
- package/dist/service/wafv2/command/web-acl/sim-wafv2-web-acl-commands.js +7 -5
- package/dist/service/wafv2/error/sim-wafv2.error.d.ts +11 -0
- package/dist/service/wafv2/error/sim-wafv2.error.js +13 -0
- package/package.json +1 -1
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { SimAthenaEngineRow } from "./sim-athena-engine-row.js";
|
|
2
|
+
/**
|
|
3
|
+
* How one table's JSON records reach its columns.
|
|
4
|
+
*
|
|
5
|
+
* `mappings` holds the column names the OpenX SerDe's `mapping.<column>`
|
|
6
|
+
* parameters declare, each against the record key it reads. A table declaring
|
|
7
|
+
* none leaves it empty, and every column is read by its own name.
|
|
8
|
+
*
|
|
9
|
+
* `caseInsensitive` is the SerDe's own `case.insensitive`, on unless a table
|
|
10
|
+
* turns it off. The SerDe folds a record's keys before looking one up. A
|
|
11
|
+
* mapping matches a key of any case until a table declares `FALSE`.
|
|
12
|
+
*/
|
|
13
|
+
export interface SimAthenaJsonFormat {
|
|
14
|
+
readonly mappings: ReadonlyMap<string, string>;
|
|
15
|
+
readonly caseInsensitive: boolean;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* One object of JSON lines, read into rows.
|
|
19
|
+
*
|
|
20
|
+
* A nested object or array is kept as its JSON text, which is what makes
|
|
21
|
+
* `json_extract_scalar` and `cardinality` reach into it.
|
|
22
|
+
*/
|
|
23
|
+
export declare function simAthenaJsonRows(text: string, format: SimAthenaJsonFormat): readonly SimAthenaEngineRow[];
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One object of JSON lines, read into rows.
|
|
3
|
+
*
|
|
4
|
+
* A nested object or array is kept as its JSON text, which is what makes
|
|
5
|
+
* `json_extract_scalar` and `cardinality` reach into it.
|
|
6
|
+
*/
|
|
7
|
+
export function simAthenaJsonRows(text, format) {
|
|
8
|
+
return text
|
|
9
|
+
.split("\n")
|
|
10
|
+
.filter((line) => line.trim().length > 0)
|
|
11
|
+
.map((line) => JSON.parse(line))
|
|
12
|
+
.map((record) => mappedRow(record, format));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One record with its mapped columns laid over it.
|
|
16
|
+
*
|
|
17
|
+
* The record is kept whole underneath. A column no mapping names still reads
|
|
18
|
+
* by its own name. A mapped column is written whether or not the record
|
|
19
|
+
* carries the key, since the mapping is where that column reads from.
|
|
20
|
+
*/
|
|
21
|
+
function mappedRow(record, format) {
|
|
22
|
+
if (format.mappings.size === 0) {
|
|
23
|
+
return record;
|
|
24
|
+
}
|
|
25
|
+
const row = { ...record };
|
|
26
|
+
for (const [column, key] of format.mappings) {
|
|
27
|
+
row[column] = mappedValue(record, key, format.caseInsensitive);
|
|
28
|
+
}
|
|
29
|
+
return row;
|
|
30
|
+
}
|
|
31
|
+
function mappedValue(record, key, caseInsensitive) {
|
|
32
|
+
const exact = record[key];
|
|
33
|
+
if (exact !== undefined) {
|
|
34
|
+
return exact;
|
|
35
|
+
}
|
|
36
|
+
if (!caseInsensitive) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
const wanted = key.toLowerCase();
|
|
40
|
+
for (const [held, value] of Object.entries(record)) {
|
|
41
|
+
if (held.toLowerCase() === wanted) {
|
|
42
|
+
return value;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One object's bytes, decompressed by the codec its key names.
|
|
3
|
+
*
|
|
4
|
+
* Athena reads the file extension, and so does this. A key ending `.gz` is
|
|
5
|
+
* gzip whatever its bytes hold, and a key ending anything else is text. The
|
|
6
|
+
* magic number would be the other way to tell, and it would read an object
|
|
7
|
+
* real Athena skips.
|
|
8
|
+
*
|
|
9
|
+
* A key naming a codec this simulation has no decompressor for raises, and the
|
|
10
|
+
* engine turns the query down. The declaration a test wrote answers it, which
|
|
11
|
+
* is what an object the engine cannot open already does.
|
|
12
|
+
*/
|
|
13
|
+
export declare function simAthenaDecompressedBytes(key: string, bytes: Buffer): Buffer;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { gunzipSync, inflateSync, zstdDecompressSync } from "node:zlib";
|
|
2
|
+
import { SimAthenaSetUpError } from "../error/sim-athena.error.js";
|
|
3
|
+
/** The file extensions naming a codec, against what decompresses each one. */
|
|
4
|
+
const codecs = new Map([
|
|
5
|
+
["gz", gunzipSync],
|
|
6
|
+
["zst", zstdDecompressSync],
|
|
7
|
+
["deflate", inflateSync],
|
|
8
|
+
]);
|
|
9
|
+
/**
|
|
10
|
+
* The file extensions naming a codec nothing here decompresses.
|
|
11
|
+
*
|
|
12
|
+
* Node's standard library has gzip, zstd and deflate. The rest would need a
|
|
13
|
+
* dependency, and this package takes none for a test to install.
|
|
14
|
+
*/
|
|
15
|
+
const unreadableCodecs = new Set(["bz2", "bzip2", "lz4", "lzo", "snappy"]);
|
|
16
|
+
/**
|
|
17
|
+
* One object's bytes, decompressed by the codec its key names.
|
|
18
|
+
*
|
|
19
|
+
* Athena reads the file extension, and so does this. A key ending `.gz` is
|
|
20
|
+
* gzip whatever its bytes hold, and a key ending anything else is text. The
|
|
21
|
+
* magic number would be the other way to tell, and it would read an object
|
|
22
|
+
* real Athena skips.
|
|
23
|
+
*
|
|
24
|
+
* A key naming a codec this simulation has no decompressor for raises, and the
|
|
25
|
+
* engine turns the query down. The declaration a test wrote answers it, which
|
|
26
|
+
* is what an object the engine cannot open already does.
|
|
27
|
+
*/
|
|
28
|
+
export function simAthenaDecompressedBytes(key, bytes) {
|
|
29
|
+
const name = key.slice(key.lastIndexOf("/") + 1);
|
|
30
|
+
const dot = name.lastIndexOf(".");
|
|
31
|
+
if (dot === -1) {
|
|
32
|
+
return bytes;
|
|
33
|
+
}
|
|
34
|
+
const extension = name.slice(dot + 1).toLowerCase();
|
|
35
|
+
if (unreadableCodecs.has(extension)) {
|
|
36
|
+
throw new SimAthenaSetUpError(`Unsupported sim Athena compression: ${extension}`);
|
|
37
|
+
}
|
|
38
|
+
const decompress = codecs.get(extension);
|
|
39
|
+
return decompress === undefined ? bytes : decompress(bytes);
|
|
40
|
+
}
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { simAthenaDelimitedRows, } from "./sim-athena-delimited-records.js";
|
|
2
|
+
import { simAthenaJsonRows, } from "./sim-athena-json-records.js";
|
|
3
|
+
/** The SerDe class name that takes `mapping.<column>` parameters. */
|
|
4
|
+
const openXSerDe = "org.openx.data.jsonserde.jsonserde";
|
|
2
5
|
/** The SerDe class names that mean JSON lines. */
|
|
3
6
|
const jsonSerDes = new Set([
|
|
4
|
-
|
|
7
|
+
openXSerDe,
|
|
5
8
|
"org.apache.hive.hcatalog.data.jsonserde",
|
|
6
9
|
"org.apache.hadoop.hive.serde2.jsonserde",
|
|
7
10
|
]);
|
|
11
|
+
/** What a `mapping.<column>` parameter is named with. */
|
|
12
|
+
const mappingPrefix = "mapping.";
|
|
8
13
|
/**
|
|
9
14
|
* The SerDe class names that mean delimited text, and what each one reads
|
|
10
15
|
* before the table's own parameters are applied.
|
|
@@ -39,7 +44,8 @@ export function simAthenaRecordReader(table) {
|
|
|
39
44
|
return undefined;
|
|
40
45
|
}
|
|
41
46
|
if (jsonSerDes.has(library)) {
|
|
42
|
-
|
|
47
|
+
const format = jsonFormat(table, library);
|
|
48
|
+
return (text) => simAthenaJsonRows(text, format);
|
|
43
49
|
}
|
|
44
50
|
const defaults = delimitedSerDes.get(library);
|
|
45
51
|
if (defaults === undefined) {
|
|
@@ -50,16 +56,26 @@ export function simAthenaRecordReader(table) {
|
|
|
50
56
|
return (text) => simAthenaDelimitedRows(text, format, columns);
|
|
51
57
|
}
|
|
52
58
|
/**
|
|
53
|
-
* JSON
|
|
59
|
+
* How one table's JSON records reach its columns.
|
|
54
60
|
*
|
|
55
|
-
*
|
|
56
|
-
* `
|
|
61
|
+
* Only the OpenX SerDe takes the mappings. The Hive JSON SerDes have no
|
|
62
|
+
* `mapping` property, and a table declaring one against them reads by its
|
|
63
|
+
* column names on real Athena the same as here.
|
|
57
64
|
*/
|
|
58
|
-
function
|
|
59
|
-
|
|
60
|
-
.
|
|
61
|
-
|
|
62
|
-
|
|
65
|
+
function jsonFormat(table, library) {
|
|
66
|
+
const parameters = library === openXSerDe
|
|
67
|
+
? (table.storageDescriptor?.SerdeInfo?.Parameters ?? {})
|
|
68
|
+
: {};
|
|
69
|
+
const mappings = new Map();
|
|
70
|
+
for (const [name, key] of Object.entries(parameters)) {
|
|
71
|
+
if (name.toLowerCase().startsWith(mappingPrefix)) {
|
|
72
|
+
mappings.set(name.slice(mappingPrefix.length), key);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return {
|
|
76
|
+
mappings,
|
|
77
|
+
caseInsensitive: parameters["case.insensitive"]?.toLowerCase() !== "false",
|
|
78
|
+
};
|
|
63
79
|
}
|
|
64
80
|
function delimitedFormat(table, defaults) {
|
|
65
81
|
const parameters = table.storageDescriptor?.SerdeInfo?.Parameters ?? {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** One pattern and the flags to compile it with. */
|
|
2
|
+
export interface SimAthenaLiftedPattern {
|
|
3
|
+
readonly pattern: string;
|
|
4
|
+
readonly flags: string;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* One pattern with the flags Java writes inside it lifted out beside it.
|
|
8
|
+
*
|
|
9
|
+
* Athena's regular expressions are Joni, which reads `(?i)` at the head of a
|
|
10
|
+
* pattern as a flag over the whole of it. JavaScript reads the same text as a
|
|
11
|
+
* group and refuses it. `(?i)`, `(?m)` and `(?s)` map onto flags of the same
|
|
12
|
+
* letter, and lifting them is all it takes.
|
|
13
|
+
*
|
|
14
|
+
* A flag Joni has and JavaScript has not, `(?x)` among them, stays in the
|
|
15
|
+
* pattern for `RegExp` to turn down. So does a group written anywhere but the
|
|
16
|
+
* head, since JavaScript has no way to turn a flag on part way through. The
|
|
17
|
+
* scoped form `(?i:...)` needs nothing done to it and already runs.
|
|
18
|
+
*
|
|
19
|
+
* A flag group captures nothing in Java, so taking one off the front leaves
|
|
20
|
+
* the capture groups numbered as the statement wrote them.
|
|
21
|
+
*/
|
|
22
|
+
export declare function simAthenaLiftedPattern(pattern: string, flags: string): SimAthenaLiftedPattern;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** One of Java's inline flag groups at the head of a pattern, as `(?i)`. */
|
|
2
|
+
const leadingFlag = /^\(\?[ims]+\)/u;
|
|
3
|
+
/**
|
|
4
|
+
* One pattern with the flags Java writes inside it lifted out beside it.
|
|
5
|
+
*
|
|
6
|
+
* Athena's regular expressions are Joni, which reads `(?i)` at the head of a
|
|
7
|
+
* pattern as a flag over the whole of it. JavaScript reads the same text as a
|
|
8
|
+
* group and refuses it. `(?i)`, `(?m)` and `(?s)` map onto flags of the same
|
|
9
|
+
* letter, and lifting them is all it takes.
|
|
10
|
+
*
|
|
11
|
+
* A flag Joni has and JavaScript has not, `(?x)` among them, stays in the
|
|
12
|
+
* pattern for `RegExp` to turn down. So does a group written anywhere but the
|
|
13
|
+
* head, since JavaScript has no way to turn a flag on part way through. The
|
|
14
|
+
* scoped form `(?i:...)` needs nothing done to it and already runs.
|
|
15
|
+
*
|
|
16
|
+
* A flag group captures nothing in Java, so taking one off the front leaves
|
|
17
|
+
* the capture groups numbered as the statement wrote them.
|
|
18
|
+
*/
|
|
19
|
+
export function simAthenaLiftedPattern(pattern, flags) {
|
|
20
|
+
const letters = new Set();
|
|
21
|
+
let rest = pattern;
|
|
22
|
+
let head = leadingFlag.exec(rest);
|
|
23
|
+
while (head !== null) {
|
|
24
|
+
// Everything the group holds between its `(?` and its `)`.
|
|
25
|
+
const written = head[0].slice(2, -1);
|
|
26
|
+
for (const letter of written) {
|
|
27
|
+
letters.add(letter);
|
|
28
|
+
}
|
|
29
|
+
rest = rest.slice(head[0].length);
|
|
30
|
+
head = leadingFlag.exec(rest);
|
|
31
|
+
}
|
|
32
|
+
const added = [...letters].filter((letter) => !flags.includes(letter));
|
|
33
|
+
return { pattern: rest, flags: flags + added.join("") };
|
|
34
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SimAthenaSetUpError } from "../error/sim-athena.error.js";
|
|
2
|
+
import { simAthenaLiftedPattern } from "./sim-athena-regexp-flags.js";
|
|
2
3
|
import { isExplicitNull, shimNumber, shimText, simAthenaScalarShim, } from "./sim-athena-shim-registry.js";
|
|
3
4
|
/**
|
|
4
5
|
* Trino's regular expression functions.
|
|
@@ -79,10 +80,11 @@ function expressionFor(pattern, flags = "u") {
|
|
|
79
80
|
if (pattern === undefined) {
|
|
80
81
|
return undefined;
|
|
81
82
|
}
|
|
83
|
+
const lifted = simAthenaLiftedPattern(pattern, flags);
|
|
82
84
|
try {
|
|
83
85
|
// The pattern is the query's own, which is the whole point of the function.
|
|
84
86
|
// oxlint-disable-next-line security/detect-non-literal-regexp
|
|
85
|
-
return new RegExp(pattern, flags);
|
|
87
|
+
return new RegExp(lifted.pattern, lifted.flags);
|
|
86
88
|
}
|
|
87
89
|
catch {
|
|
88
90
|
return undefined;
|
|
@@ -26,5 +26,5 @@ export interface SimAthenaTableObjects extends SimAthenaScannedObjects {
|
|
|
26
26
|
* there because nothing holds the data it would answer from.
|
|
27
27
|
*/
|
|
28
28
|
export declare function simAthenaTableObjects(s3: Partial<SimAthenaTableObjects> | undefined): SimAthenaTableObjects | undefined;
|
|
29
|
-
/** One object's bytes, read as UTF-8 text. */
|
|
29
|
+
/** One object's bytes, decompressed by its key and read as UTF-8 text. */
|
|
30
30
|
export declare function simAthenaObjectText(objects: SimAthenaTableObjects, bucket: string, key: string, caller: SimAwsCaller | undefined): Promise<string>;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { simAthenaDecompressedBytes } from "./sim-athena-object-codec.js";
|
|
1
2
|
/**
|
|
2
3
|
* Whether this simulated S3 can open an object as well as list one.
|
|
3
4
|
*
|
|
@@ -9,9 +10,10 @@ export function simAthenaTableObjects(s3) {
|
|
|
9
10
|
? undefined
|
|
10
11
|
: s3;
|
|
11
12
|
}
|
|
12
|
-
/** One object's bytes, read as UTF-8 text. */
|
|
13
|
+
/** One object's bytes, decompressed by its key and read as UTF-8 text. */
|
|
13
14
|
export async function simAthenaObjectText(objects, bucket, key, caller) {
|
|
14
15
|
const got = await objects.getObject({ input: { Bucket: bucket, Key: key } }, caller === undefined ? undefined : { caller });
|
|
15
16
|
const chunks = await Array.fromAsync(got.Body ?? []);
|
|
16
|
-
|
|
17
|
+
const bytes = simAthenaDecompressedBytes(key, Buffer.concat(chunks));
|
|
18
|
+
return new TextDecoder().decode(bytes);
|
|
17
19
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** One URI reference split the way Java's `URI` splits it. */
|
|
2
|
+
export interface SimAthenaUrlParts {
|
|
3
|
+
readonly protocol: string;
|
|
4
|
+
readonly host: string;
|
|
5
|
+
readonly port: number | null;
|
|
6
|
+
readonly path: string;
|
|
7
|
+
readonly query: string;
|
|
8
|
+
readonly fragment: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* One URI reference split into its parts, or nothing where it is no reference.
|
|
12
|
+
*
|
|
13
|
+
* Trino reads a URL with `new URI(text)`, which takes a reference with no
|
|
14
|
+
* scheme of its own. `/reports/august?tenant=acme` is one, and it is the shape
|
|
15
|
+
* a CloudFront access log holds, since the log carries the path and the query
|
|
16
|
+
* in columns of their own and no whole URL anywhere.
|
|
17
|
+
*
|
|
18
|
+
* The parts are read off the text rather than resolved against a base. That is
|
|
19
|
+
* what `URI` does, and it keeps a relative path as the text wrote it.
|
|
20
|
+
*
|
|
21
|
+
* A part the reference leaves out comes back as the empty string, which is
|
|
22
|
+
* what Trino answers for one. The port has no empty form and answers null.
|
|
23
|
+
*/
|
|
24
|
+
export declare function simAthenaUrlParts(text: string): SimAthenaUrlParts | undefined;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A URI reference, split as RFC 3986 writes the expression for it.
|
|
3
|
+
*
|
|
4
|
+
* This is the expression the RFC prints in its own appendix, character for
|
|
5
|
+
* character. Its groups are optional and never nested, so what a reference
|
|
6
|
+
* that does not match costs in backtracking is bounded by the text's length.
|
|
7
|
+
*/
|
|
8
|
+
const reference =
|
|
9
|
+
// oxlint-disable-next-line security/detect-unsafe-regex
|
|
10
|
+
/^(?:([^:/?#]+):)?(?:\/\/([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?$/u;
|
|
11
|
+
/** The characters RFC 2396 allows, which is what Java's `URI` takes. */
|
|
12
|
+
const disallowed = /[^A-Za-z0-9;/?:@&=+$,\-_.!~*'()%[\]#]/u;
|
|
13
|
+
/** A percent naming no byte, which Java's `URI` reads as malformed. */
|
|
14
|
+
const malformedEscape = /%(?![0-9A-Fa-f]{2})/u;
|
|
15
|
+
/** A scheme starts with a letter and carries no other punctuation. */
|
|
16
|
+
const schemeName = /^[A-Za-z][A-Za-z0-9+.-]*$/u;
|
|
17
|
+
const digits = /^\d+$/u;
|
|
18
|
+
/**
|
|
19
|
+
* One URI reference split into its parts, or nothing where it is no reference.
|
|
20
|
+
*
|
|
21
|
+
* Trino reads a URL with `new URI(text)`, which takes a reference with no
|
|
22
|
+
* scheme of its own. `/reports/august?tenant=acme` is one, and it is the shape
|
|
23
|
+
* a CloudFront access log holds, since the log carries the path and the query
|
|
24
|
+
* in columns of their own and no whole URL anywhere.
|
|
25
|
+
*
|
|
26
|
+
* The parts are read off the text rather than resolved against a base. That is
|
|
27
|
+
* what `URI` does, and it keeps a relative path as the text wrote it.
|
|
28
|
+
*
|
|
29
|
+
* A part the reference leaves out comes back as the empty string, which is
|
|
30
|
+
* what Trino answers for one. The port has no empty form and answers null.
|
|
31
|
+
*/
|
|
32
|
+
export function simAthenaUrlParts(text) {
|
|
33
|
+
if (disallowed.test(text) || malformedEscape.test(text)) {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
const found = reference.exec(text);
|
|
37
|
+
if (found === null) {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
const scheme = found.at(1);
|
|
41
|
+
if (scheme !== undefined && !schemeName.test(scheme)) {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
const authority = found.at(2);
|
|
45
|
+
const path = found.at(3) ?? "";
|
|
46
|
+
// A scheme followed by anything but a slash is an opaque URI, and Java reads
|
|
47
|
+
// neither a path nor a query out of one. `mailto:a@b` is the case.
|
|
48
|
+
const opaque = scheme !== undefined && authority === undefined && !path.startsWith("/");
|
|
49
|
+
return {
|
|
50
|
+
protocol: scheme ?? "",
|
|
51
|
+
...hostAndPort(authority),
|
|
52
|
+
path: opaque ? "" : path,
|
|
53
|
+
query: opaque ? "" : (found.at(4) ?? ""),
|
|
54
|
+
fragment: found.at(5) ?? "",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The host and the port one authority names.
|
|
59
|
+
*
|
|
60
|
+
* The user's own credentials sit before an `@` and are no part of the host, so
|
|
61
|
+
* the digits in `user:1234@rain.example` are no port. A colon with anything
|
|
62
|
+
* but digits after it belongs to the host, which is what keeps the one inside
|
|
63
|
+
* an IPv6 literal out of the port.
|
|
64
|
+
*/
|
|
65
|
+
function hostAndPort(authority) {
|
|
66
|
+
if (authority === undefined) {
|
|
67
|
+
return { host: "", port: null };
|
|
68
|
+
}
|
|
69
|
+
const written = authority.slice(authority.lastIndexOf("@") + 1);
|
|
70
|
+
const colon = written.lastIndexOf(":");
|
|
71
|
+
const port = written.slice(colon + 1);
|
|
72
|
+
if (colon === -1 || !digits.test(port)) {
|
|
73
|
+
return { host: written, port: null };
|
|
74
|
+
}
|
|
75
|
+
return { host: written.slice(0, colon), port: Number(port) };
|
|
76
|
+
}
|
|
@@ -2,8 +2,9 @@ import type { DatabaseSync } from "node:sqlite";
|
|
|
2
2
|
/**
|
|
3
3
|
* Trino's URL functions, which a query over access logs reaches for.
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* The extract functions answer null over text that is no URI reference, and so
|
|
6
|
+
* does Trino. Each one is `neverFails` there and answers null off a `URI` that
|
|
7
|
+
* would not parse.
|
|
7
8
|
*
|
|
8
9
|
* Trino answers with an empty string for a part the URL leaves out, apart from
|
|
9
10
|
* the port, which has no empty form and answers null.
|
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
import { shimText, simAthenaScalarShim } from "./sim-athena-shim-registry.js";
|
|
2
|
-
|
|
2
|
+
import { simAthenaUrlParts, } from "./sim-athena-url-parts.js";
|
|
3
|
+
/** What each `url_extract` function reads off a split reference. */
|
|
3
4
|
const parts = new Map([
|
|
4
|
-
["url_extract_host", (url) => url.
|
|
5
|
-
["url_extract_path", (url) => url.
|
|
6
|
-
["url_extract_protocol", (url) => url.protocol
|
|
7
|
-
["url_extract_fragment", (url) => url.
|
|
8
|
-
["url_extract_query", (url) => url.
|
|
5
|
+
["url_extract_host", (url) => url.host],
|
|
6
|
+
["url_extract_path", (url) => url.path],
|
|
7
|
+
["url_extract_protocol", (url) => url.protocol],
|
|
8
|
+
["url_extract_fragment", (url) => url.fragment],
|
|
9
|
+
["url_extract_query", (url) => url.query],
|
|
10
|
+
["url_extract_port", (url) => url.port],
|
|
9
11
|
]);
|
|
10
12
|
/**
|
|
11
13
|
* Trino's URL functions, which a query over access logs reaches for.
|
|
12
14
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
+
* The extract functions answer null over text that is no URI reference, and so
|
|
16
|
+
* does Trino. Each one is `neverFails` there and answers null off a `URI` that
|
|
17
|
+
* would not parse.
|
|
15
18
|
*
|
|
16
19
|
* Trino answers with an empty string for a part the URL leaves out, apart from
|
|
17
20
|
* the port, which has no empty form and answers null.
|
|
@@ -19,39 +22,68 @@ const parts = new Map([
|
|
|
19
22
|
export function simAthenaInstallUrlShims(database) {
|
|
20
23
|
for (const [name, read] of parts) {
|
|
21
24
|
simAthenaScalarShim(database, name, (value) => {
|
|
22
|
-
const url =
|
|
25
|
+
const url = readParts(shimText(value));
|
|
23
26
|
return url === undefined ? null : read(url);
|
|
24
27
|
});
|
|
25
28
|
}
|
|
26
|
-
simAthenaScalarShim(database, "url_extract_port", (value) => writtenPort(shimText(value)));
|
|
27
29
|
simAthenaScalarShim(database, "url_extract_parameter", (value, name) => {
|
|
28
30
|
const wanted = shimText(name);
|
|
29
|
-
const url =
|
|
31
|
+
const url = readParts(shimText(value));
|
|
30
32
|
if (url === undefined || wanted === undefined) {
|
|
31
33
|
return null;
|
|
32
34
|
}
|
|
33
|
-
return url.
|
|
35
|
+
return new URLSearchParams(url.query).get(wanted);
|
|
34
36
|
});
|
|
37
|
+
simAthenaScalarShim(database, "url_decode", (value) => decoded(shimText(value)));
|
|
38
|
+
simAthenaScalarShim(database, "url_encode", (value) => encoded(shimText(value)));
|
|
35
39
|
}
|
|
36
|
-
/** Everything between the scheme and the path, which is where a port is written. */
|
|
37
|
-
const authority = /^[A-Za-z][\w+.-]*:\/\/[^/?#]*/u;
|
|
38
40
|
/**
|
|
39
|
-
*
|
|
41
|
+
* One value with its escapes read back, the way `url_decode` reads them.
|
|
40
42
|
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
43
|
+
* Trino runs `URLDecoder.decode` over UTF-8, which reads `+` as a space and
|
|
44
|
+
* `%2B` as a plus. Replacing the plus before decoding is what keeps the two
|
|
45
|
+
* apart.
|
|
46
|
+
*
|
|
47
|
+
* Trino raises over an escape that names no byte, and writes a replacement
|
|
48
|
+
* character where the bytes it names are no UTF-8. `decodeURIComponent` throws
|
|
49
|
+
* over both, and this answers null for both, the same forgiving direction the
|
|
50
|
+
* rest of the file takes.
|
|
44
51
|
*/
|
|
45
|
-
function
|
|
46
|
-
if (value === undefined
|
|
52
|
+
function decoded(value) {
|
|
53
|
+
if (value === undefined) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
return decodeURIComponent(value.replaceAll("+", " "));
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
47
60
|
return null;
|
|
48
61
|
}
|
|
49
|
-
const written = /:(\d+)$/u.exec(authority.exec(value)?.[0] ?? "")?.[1];
|
|
50
|
-
return written === undefined ? null : Number(written);
|
|
51
62
|
}
|
|
52
|
-
|
|
63
|
+
/**
|
|
64
|
+
* One value escaped for a query string, the way `url_encode` escapes it.
|
|
65
|
+
*
|
|
66
|
+
* Trino runs Guava's form-parameter escaper, which keeps `-`, `_`, `.` and `*`
|
|
67
|
+
* alone and writes a space as `+`. `encodeURIComponent` keeps five characters
|
|
68
|
+
* beyond those four, and each of those is escaped here.
|
|
69
|
+
*
|
|
70
|
+
* No escape written here carries a character a later pass looks for, and a
|
|
71
|
+
* space is the only character `encodeURIComponent` writes as `%20`, since a
|
|
72
|
+
* percent in the value has already become `%25` by then.
|
|
73
|
+
*/
|
|
74
|
+
function encoded(value) {
|
|
53
75
|
if (value === undefined) {
|
|
54
|
-
return
|
|
76
|
+
return null;
|
|
55
77
|
}
|
|
56
|
-
return
|
|
78
|
+
return encodeURIComponent(value)
|
|
79
|
+
.replaceAll("!", "%21")
|
|
80
|
+
.replaceAll("'", "%27")
|
|
81
|
+
.replaceAll("(", "%28")
|
|
82
|
+
.replaceAll(")", "%29")
|
|
83
|
+
.replaceAll("~", "%7E")
|
|
84
|
+
.replaceAll("%20", "+");
|
|
85
|
+
}
|
|
86
|
+
/** One value split into its URL parts, or nothing where it is null or no URL. */
|
|
87
|
+
function readParts(value) {
|
|
88
|
+
return value === undefined ? undefined : simAthenaUrlParts(value);
|
|
57
89
|
}
|
|
@@ -34,5 +34,7 @@ export declare function anEngineSimulation(clock?: SimClock): Promise<SimAthenaE
|
|
|
34
34
|
export declare function aCatalogTable(simAws: SimAws, table: SimAthenaEngineTableInput): void;
|
|
35
35
|
/** Put one object of literal text under the logs Bucket. */
|
|
36
36
|
export declare function aSeededObject(simAws: SimAws, key: string, body: string): Promise<void>;
|
|
37
|
+
/** Put one object of raw bytes under the logs Bucket. */
|
|
38
|
+
export declare function aSeededBytes(simAws: SimAws, key: string, body: Uint8Array): Promise<void>;
|
|
37
39
|
/** Put one object of JSON lines under the logs Bucket. */
|
|
38
40
|
export declare function aSeededJson(simAws: SimAws, key: string, records: readonly Record<string, unknown>[]): Promise<void>;
|
|
@@ -58,6 +58,12 @@ export async function aSeededObject(simAws, key, body) {
|
|
|
58
58
|
.s3()
|
|
59
59
|
.putObject({ input: { Bucket: logsBucket, Key: key, Body: body } });
|
|
60
60
|
}
|
|
61
|
+
/** Put one object of raw bytes under the logs Bucket. */
|
|
62
|
+
export async function aSeededBytes(simAws, key, body) {
|
|
63
|
+
await simAws
|
|
64
|
+
.s3()
|
|
65
|
+
.putObject({ input: { Bucket: logsBucket, Key: key, Body: body } });
|
|
66
|
+
}
|
|
61
67
|
/** Put one object of JSON lines under the logs Bucket. */
|
|
62
68
|
export async function aSeededJson(simAws, key, records) {
|
|
63
69
|
const lines = records.map((record) => JSON.stringify(record));
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { SimAthenaSqlToken } from "./sim-athena-sql-tokens.js";
|
|
2
|
+
/**
|
|
3
|
+
* The columns a `NOT` in this query could have negated.
|
|
4
|
+
*
|
|
5
|
+
* A negated value names the partition a query does not want, so a column under
|
|
6
|
+
* a `NOT` is left unconstrained. A query filtering bots out of a day's logs
|
|
7
|
+
* negates nothing about the day, and reading how far each `NOT` reaches is
|
|
8
|
+
* what tells the two apart.
|
|
9
|
+
*
|
|
10
|
+
* An infix `NOT`, as in `day NOT IN ('a')`, is read as reaching nothing. Its
|
|
11
|
+
* own column keeps whatever else the statement constrains it to, and that is
|
|
12
|
+
* always a superset of what Athena reads, since `day = 'a' AND day NOT IN (x)`
|
|
13
|
+
* can only ever narrow. The query applies the exclusion itself once the rows
|
|
14
|
+
* are in. Naming the column here would drop its filter and widen the scan to
|
|
15
|
+
* every partition.
|
|
16
|
+
*/
|
|
17
|
+
export declare function simAthenaNegatedColumns(tokens: readonly SimAthenaSqlToken[]): ReadonlySet<string>;
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The columns a `NOT` in this query could have negated.
|
|
3
|
+
*
|
|
4
|
+
* A negated value names the partition a query does not want, so a column under
|
|
5
|
+
* a `NOT` is left unconstrained. A query filtering bots out of a day's logs
|
|
6
|
+
* negates nothing about the day, and reading how far each `NOT` reaches is
|
|
7
|
+
* what tells the two apart.
|
|
8
|
+
*
|
|
9
|
+
* An infix `NOT`, as in `day NOT IN ('a')`, is read as reaching nothing. Its
|
|
10
|
+
* own column keeps whatever else the statement constrains it to, and that is
|
|
11
|
+
* always a superset of what Athena reads, since `day = 'a' AND day NOT IN (x)`
|
|
12
|
+
* can only ever narrow. The query applies the exclusion itself once the rows
|
|
13
|
+
* are in. Naming the column here would drop its filter and widen the scan to
|
|
14
|
+
* every partition.
|
|
15
|
+
*/
|
|
16
|
+
export function simAthenaNegatedColumns(tokens) {
|
|
17
|
+
const negated = new Set();
|
|
18
|
+
for (const [position, token] of tokens.entries()) {
|
|
19
|
+
if (token.kind === "word" && token.text === "not") {
|
|
20
|
+
for (const name of namesUnder(tokens, position)) {
|
|
21
|
+
negated.add(name.toLowerCase());
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return negated;
|
|
26
|
+
}
|
|
27
|
+
/** The words a `NOT` reaches no further than, `OR` aside. */
|
|
28
|
+
const boundary = new Set([
|
|
29
|
+
"and",
|
|
30
|
+
"except",
|
|
31
|
+
"from",
|
|
32
|
+
"group",
|
|
33
|
+
"having",
|
|
34
|
+
"intersect",
|
|
35
|
+
"limit",
|
|
36
|
+
"offset",
|
|
37
|
+
"order",
|
|
38
|
+
"union",
|
|
39
|
+
"where",
|
|
40
|
+
"window",
|
|
41
|
+
]);
|
|
42
|
+
/**
|
|
43
|
+
* The identifiers one `NOT` reaches.
|
|
44
|
+
*
|
|
45
|
+
* `NOT` binds tighter than `AND`, so its reach ends at the next of the words
|
|
46
|
+
* above, outside any brackets, and at the bracket the `NOT` itself sits in.
|
|
47
|
+
* `OR` is absent from that list because a query carrying one is left
|
|
48
|
+
* unfiltered before this runs.
|
|
49
|
+
*
|
|
50
|
+
* Nothing here parses, so a name a `NOT` could not really have reached is
|
|
51
|
+
* dropped anyway. That direction only ever leaves a column unfiltered, which
|
|
52
|
+
* is the answer a query with no filter gets.
|
|
53
|
+
*/
|
|
54
|
+
function namesUnder(tokens, position) {
|
|
55
|
+
const names = [];
|
|
56
|
+
let depth = 0;
|
|
57
|
+
let cursor = position + 1;
|
|
58
|
+
while (cursor < tokens.length) {
|
|
59
|
+
const token = tokens.at(cursor);
|
|
60
|
+
if (token?.kind === "symbol" && token.text === "(") {
|
|
61
|
+
depth += 1;
|
|
62
|
+
}
|
|
63
|
+
else if (token?.kind === "symbol" && token.text === ")") {
|
|
64
|
+
if (depth === 0) {
|
|
65
|
+
return names;
|
|
66
|
+
}
|
|
67
|
+
depth -= 1;
|
|
68
|
+
}
|
|
69
|
+
else if (depth === 0 &&
|
|
70
|
+
token?.kind === "word" &&
|
|
71
|
+
boundary.has(token.text)) {
|
|
72
|
+
return names;
|
|
73
|
+
}
|
|
74
|
+
else if (token?.kind === "word" || token?.kind === "quoted") {
|
|
75
|
+
names.push(token.text);
|
|
76
|
+
}
|
|
77
|
+
cursor += 1;
|
|
78
|
+
}
|
|
79
|
+
return names;
|
|
80
|
+
}
|
|
@@ -2,10 +2,13 @@
|
|
|
2
2
|
* The partition values a query's `WHERE` clause pins down.
|
|
3
3
|
*
|
|
4
4
|
* Only two shapes are read, `column = 'value'` and `column IN ('a', 'b')`, and
|
|
5
|
-
* they are read wherever they appear. A query carrying `OR`
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* they are read wherever they appear. A query carrying `OR` anywhere is left
|
|
6
|
+
* unfiltered, since a value under one arm constrains nothing on its own.
|
|
7
|
+
*
|
|
8
|
+
* A `NOT` leaves the columns it reaches unconstrained and the rest alone. A
|
|
9
|
+
* negated value names the partition the query does not want, and reading it as
|
|
10
|
+
* a constraint would answer from the wrong prefixes. A negation on any other
|
|
11
|
+
* column leaves the partition constraints as true as they were.
|
|
9
12
|
*
|
|
10
13
|
* Two constraints on one column are intersected, since a query carrying both
|
|
11
14
|
* wants the rows they agree on.
|
|
@@ -1,12 +1,16 @@
|
|
|
1
|
+
import { simAthenaNegatedColumns } from "./sim-athena-negated-columns.js";
|
|
1
2
|
import { simAthenaSqlTokens } from "./sim-athena-sql-tokens.js";
|
|
2
3
|
/**
|
|
3
4
|
* The partition values a query's `WHERE` clause pins down.
|
|
4
5
|
*
|
|
5
6
|
* Only two shapes are read, `column = 'value'` and `column IN ('a', 'b')`, and
|
|
6
|
-
* they are read wherever they appear. A query carrying `OR`
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* they are read wherever they appear. A query carrying `OR` anywhere is left
|
|
8
|
+
* unfiltered, since a value under one arm constrains nothing on its own.
|
|
9
|
+
*
|
|
10
|
+
* A `NOT` leaves the columns it reaches unconstrained and the rest alone. A
|
|
11
|
+
* negated value names the partition the query does not want, and reading it as
|
|
12
|
+
* a constraint would answer from the wrong prefixes. A negation on any other
|
|
13
|
+
* column leaves the partition constraints as true as they were.
|
|
10
14
|
*
|
|
11
15
|
* Two constraints on one column are intersected, since a query carrying both
|
|
12
16
|
* wants the rows they agree on.
|
|
@@ -52,6 +56,9 @@ export function simAthenaPartitionFilters(sql) {
|
|
|
52
56
|
}
|
|
53
57
|
}
|
|
54
58
|
}
|
|
59
|
+
for (const name of simAthenaNegatedColumns(tokens)) {
|
|
60
|
+
byColumn.delete(name);
|
|
61
|
+
}
|
|
55
62
|
return new SimAthenaPartitionFilters(byColumn);
|
|
56
63
|
}
|
|
57
64
|
/**
|
|
@@ -68,7 +75,7 @@ function narrow(byColumn, columnName, values) {
|
|
|
68
75
|
byColumn.set(name, already.filter((value) => wanted.has(value)));
|
|
69
76
|
}
|
|
70
77
|
function isUnreadableTerm(token) {
|
|
71
|
-
return token.kind === "word" &&
|
|
78
|
+
return token.kind === "word" && token.text === "or";
|
|
72
79
|
}
|
|
73
80
|
/**
|
|
74
81
|
* Read `('a', 'b')` starting at its opening bracket.
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SimWafIpSet, requiredSimWafIpAddressVersion, } from "../../ip-set/sim-waf-ip-set.js";
|
|
2
2
|
import { requiredSimWafScope } from "../../scope/sim-waf-scope.js";
|
|
3
3
|
import { SimWafPage } from "../sim-wafv2-page.js";
|
|
4
|
-
import { refuseSimWafTags, requiredSimWafName } from "../sim-wafv2-input.js";
|
|
4
|
+
import { checkedSimWafDescription, refuseSimWafTags, requiredSimWafName, } from "../sim-wafv2-input.js";
|
|
5
5
|
import { requireSimWafResource } from "../sim-wafv2-resource-lookup.js";
|
|
6
6
|
/**
|
|
7
7
|
* The commands that make, read, list and remove IP sets.
|
|
@@ -29,7 +29,7 @@ export class SimWafIpSetCommands {
|
|
|
29
29
|
name: requiredSimWafName(input.Name),
|
|
30
30
|
scope: requiredSimWafScope(input.Scope, this.#accountRegionScope.regionName),
|
|
31
31
|
accountRegionScope: this.#accountRegionScope,
|
|
32
|
-
description: input.Description,
|
|
32
|
+
description: checkedSimWafDescription(input.Description),
|
|
33
33
|
ipAddressVersion: requiredSimWafIpAddressVersion(input.IPAddressVersion),
|
|
34
34
|
addresses: input.Addresses ?? [],
|
|
35
35
|
});
|
|
@@ -59,10 +59,11 @@ export class SimWafIpSetCommands {
|
|
|
59
59
|
*/
|
|
60
60
|
updateIpSet(command, options) {
|
|
61
61
|
const { input } = command;
|
|
62
|
+
const description = checkedSimWafDescription(input.Description);
|
|
62
63
|
const ipSet = this.require(input, "wafv2:UpdateIPSet", options);
|
|
63
64
|
ipSet.replaceAddresses({
|
|
64
65
|
addresses: input.Addresses ?? [],
|
|
65
|
-
description
|
|
66
|
+
description,
|
|
66
67
|
lockToken: input.LockToken,
|
|
67
68
|
});
|
|
68
69
|
return { $metadata: {}, NextLockToken: ipSet.lockToken };
|
package/dist/service/wafv2/command/regex-pattern-set/sim-wafv2-regex-pattern-set-commands.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { SimWafRegexPatternSet } from "../../regex-pattern-set/sim-waf-regex-pattern-set.js";
|
|
2
2
|
import { requiredSimWafScope } from "../../scope/sim-waf-scope.js";
|
|
3
3
|
import { SimWafPage } from "../sim-wafv2-page.js";
|
|
4
|
-
import { refuseSimWafTags, requiredSimWafName } from "../sim-wafv2-input.js";
|
|
4
|
+
import { checkedSimWafDescription, refuseSimWafTags, requiredSimWafName, } from "../sim-wafv2-input.js";
|
|
5
5
|
import { requireSimWafResource } from "../sim-wafv2-resource-lookup.js";
|
|
6
6
|
/**
|
|
7
7
|
* The commands that make, read, list and remove regex pattern sets.
|
|
@@ -29,7 +29,7 @@ export class SimWafRegexPatternSetCommands {
|
|
|
29
29
|
name: requiredSimWafName(input.Name),
|
|
30
30
|
scope: requiredSimWafScope(input.Scope, this.#accountRegionScope.regionName),
|
|
31
31
|
accountRegionScope: this.#accountRegionScope,
|
|
32
|
-
description: input.Description,
|
|
32
|
+
description: checkedSimWafDescription(input.Description),
|
|
33
33
|
regularExpressions: input.RegularExpressionList ?? [],
|
|
34
34
|
});
|
|
35
35
|
this.#authorizer.authorizeResource("wafv2:CreateRegexPatternSet", patternSet.arn, options?.caller);
|
|
@@ -66,10 +66,11 @@ export class SimWafRegexPatternSetCommands {
|
|
|
66
66
|
*/
|
|
67
67
|
updateRegexPatternSet(command, options) {
|
|
68
68
|
const { input } = command;
|
|
69
|
+
const description = checkedSimWafDescription(input.Description);
|
|
69
70
|
const patternSet = this.require(input, "wafv2:UpdateRegexPatternSet", options);
|
|
70
71
|
patternSet.replaceExpressions({
|
|
71
72
|
regularExpressions: input.RegularExpressionList ?? [],
|
|
72
|
-
description
|
|
73
|
+
description,
|
|
73
74
|
lockToken: input.LockToken,
|
|
74
75
|
});
|
|
75
76
|
return { $metadata: {}, NextLockToken: patternSet.lockToken };
|
|
@@ -24,3 +24,18 @@ export declare function refuseSimWafTags(tags: readonly unknown[] | undefined, o
|
|
|
24
24
|
* by ARN, and neither has anything else to fall back on.
|
|
25
25
|
*/
|
|
26
26
|
export declare function requiredSimWafArn(arn: string | undefined, parameter: string): string;
|
|
27
|
+
/**
|
|
28
|
+
* Read the description a write gave, refusing one WAFv2 will not store.
|
|
29
|
+
*
|
|
30
|
+
* The empty string is the one worth catching. Code that reads a resource,
|
|
31
|
+
* changes part of it and writes the rest back hands the description straight
|
|
32
|
+
* through, and AWS answers `""` for some resources nobody has described. WAFv2
|
|
33
|
+
* refuses that write. A simulation that took it would leave the failure for
|
|
34
|
+
* the account to report.
|
|
35
|
+
*
|
|
36
|
+
* The two constraints are checked apart because AWS checks them apart. The
|
|
37
|
+
* pattern matches three characters at the shortest. `ab` is long enough for
|
|
38
|
+
* the length and still refused, and `""` fails both at once, which is how one
|
|
39
|
+
* message comes to report two errors.
|
|
40
|
+
*/
|
|
41
|
+
export declare function checkedSimWafDescription(description: string | undefined): string | undefined;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { SimWafInvalidParameterException, SimWafUnsimulatedInputException, } from "../error/sim-wafv2.error.js";
|
|
1
|
+
import { SimWafInvalidParameterException, SimWafUnsimulatedInputException, SimWafValidationException, } from "../error/sim-wafv2.error.js";
|
|
2
2
|
/**
|
|
3
3
|
* Read the name a request named, refusing a request with none.
|
|
4
4
|
*/
|
|
@@ -47,3 +47,68 @@ export function requiredSimWafArn(arn, parameter) {
|
|
|
47
47
|
}
|
|
48
48
|
return arn;
|
|
49
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* The shape WAFv2 documents for the description of a web ACL, an IP set and a
|
|
52
|
+
* regex pattern set.
|
|
53
|
+
*
|
|
54
|
+
* The refusal below quotes this expression back, the way WAFv2 quotes its own.
|
|
55
|
+
* WAFv2 writes the dots inside the character classes escaped and this does
|
|
56
|
+
* not, which is the same set of characters written two ways.
|
|
57
|
+
*/
|
|
58
|
+
const descriptionExpression = /^[\w+=:#@/\-,.][\w+=:#@/\-,.\s]+[\w+=:#@/\-,.]$/;
|
|
59
|
+
const descriptionMaxLength = 256;
|
|
60
|
+
/**
|
|
61
|
+
* Read the description a write gave, refusing one WAFv2 will not store.
|
|
62
|
+
*
|
|
63
|
+
* The empty string is the one worth catching. Code that reads a resource,
|
|
64
|
+
* changes part of it and writes the rest back hands the description straight
|
|
65
|
+
* through, and AWS answers `""` for some resources nobody has described. WAFv2
|
|
66
|
+
* refuses that write. A simulation that took it would leave the failure for
|
|
67
|
+
* the account to report.
|
|
68
|
+
*
|
|
69
|
+
* The two constraints are checked apart because AWS checks them apart. The
|
|
70
|
+
* pattern matches three characters at the shortest. `ab` is long enough for
|
|
71
|
+
* the length and still refused, and `""` fails both at once, which is how one
|
|
72
|
+
* message comes to report two errors.
|
|
73
|
+
*/
|
|
74
|
+
export function checkedSimWafDescription(description) {
|
|
75
|
+
if (description === undefined) {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
const failures = [
|
|
79
|
+
...lengthFailures(description),
|
|
80
|
+
...patternFailures(description),
|
|
81
|
+
];
|
|
82
|
+
if (failures.length === 0) {
|
|
83
|
+
return description;
|
|
84
|
+
}
|
|
85
|
+
throw new SimWafValidationException(`${failures.length} validation error${failures.length === 1 ? "" : "s"} detected: ${failures
|
|
86
|
+
.map((failure) => `Value '${description}' at 'description' failed to satisfy ` +
|
|
87
|
+
`constraint: ${failure}`)
|
|
88
|
+
.join("; ")}`);
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* What the length of a description falls foul of, if anything.
|
|
92
|
+
*/
|
|
93
|
+
function lengthFailures(description) {
|
|
94
|
+
if (description.length === 0) {
|
|
95
|
+
return ["Member must have length greater than or equal to 1"];
|
|
96
|
+
}
|
|
97
|
+
if (description.length > descriptionMaxLength) {
|
|
98
|
+
return [
|
|
99
|
+
`Member must have length less than or equal to ${descriptionMaxLength}`,
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
return [];
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* What the shape of a description falls foul of, if anything.
|
|
106
|
+
*/
|
|
107
|
+
function patternFailures(description) {
|
|
108
|
+
if (descriptionExpression.test(description)) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
return [
|
|
112
|
+
`Member must satisfy regular expression pattern: ${descriptionExpression.source}`,
|
|
113
|
+
];
|
|
114
|
+
}
|
|
@@ -2,7 +2,7 @@ import { SimWafAssociatedItemException } from "../../error/sim-wafv2.error.js";
|
|
|
2
2
|
import { requiredSimWafScope } from "../../scope/sim-waf-scope.js";
|
|
3
3
|
import { SimWafWebAcl } from "../../web-acl/sim-waf-web-acl.js";
|
|
4
4
|
import { SimWafPage } from "../sim-wafv2-page.js";
|
|
5
|
-
import { requiredSimWafName } from "../sim-wafv2-input.js";
|
|
5
|
+
import { checkedSimWafDescription, requiredSimWafName, } from "../sim-wafv2-input.js";
|
|
6
6
|
import { requireSimWafResource, } from "../sim-wafv2-resource-lookup.js";
|
|
7
7
|
import { refuseUnsimulatedSimWafWebAclInput } from "./sim-wafv2-unsimulated-web-acl-input.js";
|
|
8
8
|
import { simWafWebAclOutput } from "./sim-waf-web-acl-output.js";
|
|
@@ -38,12 +38,13 @@ export class SimWafWebAclCommands {
|
|
|
38
38
|
const name = requiredSimWafName(input.Name);
|
|
39
39
|
const scope = requiredSimWafScope(input.Scope, this.#accountRegionScope.regionName);
|
|
40
40
|
refuseUnsimulatedSimWafWebAclInput(input, "CreateWebACL");
|
|
41
|
+
const configuration = configurationOf(input);
|
|
41
42
|
const webAcl = new SimWafWebAcl({
|
|
42
43
|
name,
|
|
43
44
|
scope,
|
|
44
45
|
accountRegionScope: this.#accountRegionScope,
|
|
45
|
-
description:
|
|
46
|
-
configuration
|
|
46
|
+
description: configuration.description,
|
|
47
|
+
configuration,
|
|
47
48
|
regexPatternSets: this.#regexPatternSets,
|
|
48
49
|
managedRules: this.#managedRules,
|
|
49
50
|
clock: this.#clock,
|
|
@@ -68,8 +69,9 @@ export class SimWafWebAclCommands {
|
|
|
68
69
|
updateWebAcl(command, options) {
|
|
69
70
|
const { input } = command;
|
|
70
71
|
refuseUnsimulatedSimWafWebAclInput(input, "UpdateWebACL");
|
|
72
|
+
const configuration = configurationOf(input);
|
|
71
73
|
const webAcl = this.require(input, "wafv2:UpdateWebACL", options);
|
|
72
|
-
webAcl.reconfigure(
|
|
74
|
+
webAcl.reconfigure(configuration, input.LockToken);
|
|
73
75
|
return { $metadata: {}, NextLockToken: webAcl.lockToken };
|
|
74
76
|
}
|
|
75
77
|
/**
|
|
@@ -134,6 +136,6 @@ function configurationOf(input) {
|
|
|
134
136
|
rules: input.Rules,
|
|
135
137
|
customResponseBodies: input.CustomResponseBodies,
|
|
136
138
|
visibilityConfig: input.VisibilityConfig,
|
|
137
|
-
description: input.Description,
|
|
139
|
+
description: checkedSimWafDescription(input.Description),
|
|
138
140
|
};
|
|
139
141
|
}
|
|
@@ -89,6 +89,17 @@ export declare class SimWafAssociatedItemException extends SimWafError {
|
|
|
89
89
|
readonly name = "WAFAssociatedItemException";
|
|
90
90
|
constructor(message: string);
|
|
91
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* Simulated WAFv2 ValidationException error.
|
|
94
|
+
*
|
|
95
|
+
* WAFv2 checks the length and shape of a few members before the request
|
|
96
|
+
* reaches the rest of the API, and reports every failure in one message. A
|
|
97
|
+
* description of `""` fails two of those checks at once.
|
|
98
|
+
*/
|
|
99
|
+
export declare class SimWafValidationException extends SimWafError {
|
|
100
|
+
readonly name = "ValidationException";
|
|
101
|
+
constructor(message: string);
|
|
102
|
+
}
|
|
92
103
|
/**
|
|
93
104
|
* A match declared against this simulation that it cannot answer with.
|
|
94
105
|
*
|
|
@@ -100,6 +100,19 @@ export class SimWafAssociatedItemException extends SimWafError {
|
|
|
100
100
|
super(message, { httpStatusCode: 400 });
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Simulated WAFv2 ValidationException error.
|
|
105
|
+
*
|
|
106
|
+
* WAFv2 checks the length and shape of a few members before the request
|
|
107
|
+
* reaches the rest of the API, and reports every failure in one message. A
|
|
108
|
+
* description of `""` fails two of those checks at once.
|
|
109
|
+
*/
|
|
110
|
+
export class SimWafValidationException extends SimWafError {
|
|
111
|
+
name = "ValidationException";
|
|
112
|
+
constructor(message) {
|
|
113
|
+
super(message, { httpStatusCode: 400 });
|
|
114
|
+
}
|
|
115
|
+
}
|
|
103
116
|
/**
|
|
104
117
|
* A match declared against this simulation that it cannot answer with.
|
|
105
118
|
*
|
package/package.json
CHANGED