@testsmith/api-spector 0.5.5 → 0.5.6
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/out/main/chunks/{snapshots-CN8g7lqA.js → snapshots-CcvHHTnQ.js} +125 -10
- package/out/main/contract.js +7 -3
- package/out/main/index.js +2 -2
- package/out/main/runner.js +1 -1
- package/out/renderer/assets/{GraphQLEditor-CegyHBoY.js → GraphQLEditor-Caafu60-.js} +1 -1
- package/out/renderer/assets/{SoapEditor-BjZHo7Xf.js → SoapEditor-uTnyfTVp.js} +1 -1
- package/out/renderer/assets/{index-e3ZM7oe9.js → index-BKJHj4_d.js} +1000 -652
- package/out/renderer/assets/index-CMI5gtcx.css +2 -0
- package/out/renderer/index.html +2 -2
- package/package.json +1 -1
- package/out/renderer/assets/index-kNbbR_rD.css +0 -2
|
@@ -1432,6 +1432,37 @@ const ADVERSARIAL_STRINGS = [
|
|
|
1432
1432
|
["unicode", "𝔘🙈\0�"],
|
|
1433
1433
|
["whitespace", " "]
|
|
1434
1434
|
];
|
|
1435
|
+
const INJECTION_PAYLOADS = [
|
|
1436
|
+
["sql", "or-1-1", "' OR '1'='1"],
|
|
1437
|
+
["sql", "comment", "'--"],
|
|
1438
|
+
["nosql", "ne-null", '{"$ne": null}'],
|
|
1439
|
+
["nosql", "gt-empty", '{"$gt": ""}'],
|
|
1440
|
+
["command", "semicolon", "; id"],
|
|
1441
|
+
["command", "subshell", "$(id)"],
|
|
1442
|
+
["xss", "script", "<script>alert(1)<\/script>"],
|
|
1443
|
+
["xss", "img-onerror", '"><img src=x onerror=alert(1)>'],
|
|
1444
|
+
["traversal", "etc-passwd", "../../../../etc/passwd"],
|
|
1445
|
+
["template", "mustache", "{{7*7}}"],
|
|
1446
|
+
["template", "dollar", "${7*7}"],
|
|
1447
|
+
["ldap", "wildcard", "*)(uid=*"],
|
|
1448
|
+
["crlf", "header", "x\r\nX-Injected: 1"],
|
|
1449
|
+
["xxe", "entity", '<!DOCTYPE t [<!ENTITY e SYSTEM "file:///etc/passwd">]><t>&e;</t>']
|
|
1450
|
+
];
|
|
1451
|
+
const NAUGHTY_STRINGS = [
|
|
1452
|
+
["emoji", "😍👩🏽💻🏳️🌈"],
|
|
1453
|
+
["rtl-override", "txet desrever"],
|
|
1454
|
+
["zalgo", "Z͢͡a̴͈l͖g̴o"],
|
|
1455
|
+
["zero-width", "abcd\uFEFF"],
|
|
1456
|
+
["format-string", "%s%s%s%n%x%x"],
|
|
1457
|
+
["fullwidth", "12345"],
|
|
1458
|
+
["reserved-word", "NaN"],
|
|
1459
|
+
["bignum-string", "9".repeat(300)],
|
|
1460
|
+
["null-char", "a\0b"],
|
|
1461
|
+
["mixed-newlines", "a\r\nb\nc\rd"],
|
|
1462
|
+
["quotes", "'\"`"],
|
|
1463
|
+
["brace-soup", "${{{}}}[]()"],
|
|
1464
|
+
["huge", "A".repeat(5e4)]
|
|
1465
|
+
];
|
|
1435
1466
|
function clone(v) {
|
|
1436
1467
|
return JSON.parse(JSON.stringify(v));
|
|
1437
1468
|
}
|
|
@@ -1450,7 +1481,7 @@ function deleteAtPath(root, path2) {
|
|
|
1450
1481
|
delete cur[path2[path2.length - 1]];
|
|
1451
1482
|
return copy;
|
|
1452
1483
|
}
|
|
1453
|
-
function mutate(baseline, schema, label) {
|
|
1484
|
+
function mutate(baseline, schema, label, level = "standard") {
|
|
1454
1485
|
const cases = [];
|
|
1455
1486
|
const walk = (schemaNode, path2) => {
|
|
1456
1487
|
const target = [label, ...path2].join(".");
|
|
@@ -1467,9 +1498,14 @@ function mutate(baseline, schema, label) {
|
|
|
1467
1498
|
});
|
|
1468
1499
|
}
|
|
1469
1500
|
}
|
|
1470
|
-
if (
|
|
1501
|
+
if (current) {
|
|
1502
|
+
const forbidden = schemaNode["additionalProperties"] === false;
|
|
1471
1503
|
cases.push({
|
|
1472
|
-
mutation: {
|
|
1504
|
+
mutation: {
|
|
1505
|
+
target: `${target}.__fuzz_extra`,
|
|
1506
|
+
kind: forbidden ? "unexpected-field" : "extra-property",
|
|
1507
|
+
description: forbidden ? "add a field the schema forbids" : "add an unexpected extra field"
|
|
1508
|
+
},
|
|
1473
1509
|
value: setAtPath(baseline, [...path2, "__fuzz_extra"], "unexpected")
|
|
1474
1510
|
});
|
|
1475
1511
|
}
|
|
@@ -1513,9 +1549,25 @@ function mutate(baseline, schema, label) {
|
|
|
1513
1549
|
value: setAtPath(baseline, path2, "x".repeat(maxLen + 1))
|
|
1514
1550
|
});
|
|
1515
1551
|
}
|
|
1516
|
-
|
|
1552
|
+
if (level !== "basic") {
|
|
1553
|
+
for (const [name, str] of ADVERSARIAL_STRINGS) {
|
|
1554
|
+
cases.push({
|
|
1555
|
+
mutation: { target, kind: `adversarial:${name}`, description: `inject a ${name} string` },
|
|
1556
|
+
value: setAtPath(baseline, path2, str)
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
}
|
|
1560
|
+
if (level === "aggressive") {
|
|
1561
|
+
for (const [cat, name, payload] of INJECTION_PAYLOADS) {
|
|
1562
|
+
cases.push({
|
|
1563
|
+
mutation: { target, kind: `injection:${cat}:${name}`, description: `inject a ${cat} probe` },
|
|
1564
|
+
value: setAtPath(baseline, path2, payload)
|
|
1565
|
+
});
|
|
1566
|
+
}
|
|
1567
|
+
}
|
|
1568
|
+
if (level !== "basic") for (const [name, str] of NAUGHTY_STRINGS) {
|
|
1517
1569
|
cases.push({
|
|
1518
|
-
mutation: { target, kind: `
|
|
1570
|
+
mutation: { target, kind: `naughty:${name}`, description: `inject a naughty string (${name})` },
|
|
1519
1571
|
value: setAtPath(baseline, path2, str)
|
|
1520
1572
|
});
|
|
1521
1573
|
}
|
|
@@ -1583,10 +1635,11 @@ function setParam(base, key, value) {
|
|
|
1583
1635
|
function dropParam(base, key) {
|
|
1584
1636
|
return base.filter((p) => p.key !== key);
|
|
1585
1637
|
}
|
|
1586
|
-
function mutateQueryParams(base, schemas = [], label = "query") {
|
|
1638
|
+
function mutateQueryParams(base, schemas = [], label = "query", level = "standard") {
|
|
1587
1639
|
const cases = [];
|
|
1588
1640
|
const schemaByName = new Map(schemas.map((s) => [s.name, s]));
|
|
1589
1641
|
const target = (name) => `${label}.${name}`;
|
|
1642
|
+
const injectionProbes = /* @__PURE__ */ new Set(["sql", "xss", "traversal", "null-byte"]);
|
|
1590
1643
|
for (const s of schemas) {
|
|
1591
1644
|
if (s.required && base.some((p) => p.enabled && p.key === s.name)) {
|
|
1592
1645
|
cases.push({
|
|
@@ -1614,8 +1667,11 @@ function mutateQueryParams(base, schemas = [], label = "query") {
|
|
|
1614
1667
|
if (typeof min === "number") cases.push({ mutation: { target: t, kind: "below-minimum", description: `value below minimum (${min})` }, params: setParam(base, p.key, String(min - 1)) });
|
|
1615
1668
|
if (typeof max === "number") cases.push({ mutation: { target: t, kind: "above-maximum", description: `value above maximum (${max})` }, params: setParam(base, p.key, String(max + 1)) });
|
|
1616
1669
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1670
|
+
if (level !== "basic") {
|
|
1671
|
+
for (const [name, val] of QUERY_ADVERSARIAL) {
|
|
1672
|
+
if (injectionProbes.has(name) && level !== "aggressive") continue;
|
|
1673
|
+
cases.push({ mutation: { target: t, kind: `adversarial:${name}`, description: `inject a ${name} value` }, params: setParam(base, p.key, val) });
|
|
1674
|
+
}
|
|
1619
1675
|
}
|
|
1620
1676
|
}
|
|
1621
1677
|
cases.push({
|
|
@@ -1647,6 +1703,46 @@ function inferSchema(value) {
|
|
|
1647
1703
|
return { type: "string" };
|
|
1648
1704
|
}
|
|
1649
1705
|
}
|
|
1706
|
+
function duplicateKeyBody(baseline) {
|
|
1707
|
+
if (!baseline || typeof baseline !== "object" || Array.isArray(baseline)) return null;
|
|
1708
|
+
const entries = Object.entries(baseline);
|
|
1709
|
+
if (entries.length === 0) return null;
|
|
1710
|
+
const parts = entries.map(([k, v]) => `${JSON.stringify(k)}:${JSON.stringify(v)}`);
|
|
1711
|
+
const [firstKey, firstVal] = entries[0];
|
|
1712
|
+
const dupVal = typeof firstVal === "string" ? `${firstVal}-dup` : "__duplicate__";
|
|
1713
|
+
parts.push(`${JSON.stringify(firstKey)}:${JSON.stringify(dupVal)}`);
|
|
1714
|
+
return `{${parts.join(",")}}`;
|
|
1715
|
+
}
|
|
1716
|
+
function mutateHeaders(base, label = "header") {
|
|
1717
|
+
const withHeader = (key, value) => [
|
|
1718
|
+
...base.filter((h) => h.key.toLowerCase() !== key.toLowerCase()),
|
|
1719
|
+
{ key, value, enabled: true }
|
|
1720
|
+
];
|
|
1721
|
+
const cases = [];
|
|
1722
|
+
const contentTypes = [
|
|
1723
|
+
["multipart-mixed", "multipart/mixed"],
|
|
1724
|
+
["text-plain", "text/plain"],
|
|
1725
|
+
["xml", "application/xml"],
|
|
1726
|
+
["empty", ""],
|
|
1727
|
+
["wildcard", "*/*"],
|
|
1728
|
+
["bad-charset", "application/json; charset=not-a-charset"]
|
|
1729
|
+
];
|
|
1730
|
+
for (const [name, ct] of contentTypes) {
|
|
1731
|
+
cases.push({
|
|
1732
|
+
mutation: { target: `${label}.Content-Type`, kind: `header:content-type:${name}`, description: `tamper Content-Type (${ct || "empty"})` },
|
|
1733
|
+
headers: withHeader("Content-Type", ct)
|
|
1734
|
+
});
|
|
1735
|
+
}
|
|
1736
|
+
cases.push({
|
|
1737
|
+
mutation: { target: `${label}.Accept`, kind: "header:accept-unsatisfiable", description: "demand an unsatisfiable Accept" },
|
|
1738
|
+
headers: withHeader("Accept", "application/x-does-not-exist")
|
|
1739
|
+
});
|
|
1740
|
+
cases.push({
|
|
1741
|
+
mutation: { target: `${label}.X-Fuzz-Overflow`, kind: "header:oversized", description: "send an oversized header value" },
|
|
1742
|
+
headers: [...base, { key: "X-Fuzz-Overflow", value: "A".repeat(8192), enabled: true }]
|
|
1743
|
+
});
|
|
1744
|
+
return cases;
|
|
1745
|
+
}
|
|
1650
1746
|
function sampleApplied(arr, n, rng) {
|
|
1651
1747
|
if (arr.length <= n) return arr;
|
|
1652
1748
|
const a = [...arr];
|
|
@@ -1723,6 +1819,7 @@ async function runFuzz(opts) {
|
|
|
1723
1819
|
const spec = opts.specUrl || opts.specPath ? await loadSpec(opts.specUrl, opts.specPath) : null;
|
|
1724
1820
|
const seed = opts.seed ?? 1;
|
|
1725
1821
|
const casesPerOp = opts.casesPerOperation ?? 40;
|
|
1822
|
+
const level = opts.level ?? "standard";
|
|
1726
1823
|
const vars = requestExec.mergeVars(opts.envVars, opts.collectionVars ?? {}, {}, {}, await requestExec.buildDynamicVars());
|
|
1727
1824
|
const dispatcher = await requestExec.buildDispatcher(void 0, void 0);
|
|
1728
1825
|
const start = Date.now();
|
|
@@ -1767,13 +1864,30 @@ async function runFuzz(opts) {
|
|
|
1767
1864
|
}
|
|
1768
1865
|
const applied = [];
|
|
1769
1866
|
if (hasBody) {
|
|
1770
|
-
for (const c of mutate(baselineBody, bodySchema, "body")) {
|
|
1867
|
+
for (const c of mutate(baselineBody, bodySchema, "body", level)) {
|
|
1771
1868
|
applied.push({ mutation: c.mutation, bodyJson: JSON.stringify(c.value), bodyValue: c.value, params: baselineParams, isBody: true });
|
|
1772
1869
|
}
|
|
1870
|
+
if (level !== "basic") {
|
|
1871
|
+
const dupRaw = duplicateKeyBody(baselineBody);
|
|
1872
|
+
if (dupRaw) {
|
|
1873
|
+
applied.push({
|
|
1874
|
+
mutation: { target: "body", kind: "duplicate-key", description: "send the same JSON key twice" },
|
|
1875
|
+
bodyJson: dupRaw,
|
|
1876
|
+
bodyValue: baselineBody,
|
|
1877
|
+
params: baselineParams,
|
|
1878
|
+
isBody: true
|
|
1879
|
+
});
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1773
1882
|
}
|
|
1774
|
-
for (const c of mutateQueryParams(baselineParams, specCtx?.queryParams ?? [], "query")) {
|
|
1883
|
+
for (const c of mutateQueryParams(baselineParams, specCtx?.queryParams ?? [], "query", level)) {
|
|
1775
1884
|
applied.push({ mutation: c.mutation, bodyJson: baselineBodyJson, params: c.params, isBody: false });
|
|
1776
1885
|
}
|
|
1886
|
+
if (level === "aggressive") {
|
|
1887
|
+
for (const hc of mutateHeaders(req.headers ?? [], "header")) {
|
|
1888
|
+
applied.push({ mutation: hc.mutation, bodyJson: baselineBodyJson, params: baselineParams, isBody: false, headers: hc.headers });
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1777
1891
|
const cases = sampleApplied(applied, casesPerOp, makeRng(seed + 1));
|
|
1778
1892
|
const findings = [];
|
|
1779
1893
|
const trace = opts.trace ? [] : void 0;
|
|
@@ -1788,6 +1902,7 @@ async function runFuzz(opts) {
|
|
|
1788
1902
|
const fuzzReq = {
|
|
1789
1903
|
...req,
|
|
1790
1904
|
params: c.params,
|
|
1905
|
+
...c.headers ? { headers: c.headers } : {},
|
|
1791
1906
|
body: c.bodyJson !== void 0 ? { mode: "json", json: c.bodyJson } : req.body
|
|
1792
1907
|
};
|
|
1793
1908
|
const resolvedUrl = requestExec.rebaseUrl(requestExec.buildUrl(fuzzReq.url, fuzzReq.params, vars), opts.providerBaseUrl);
|
package/out/main/contract.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"use strict";
|
|
3
3
|
const promises = require("fs/promises");
|
|
4
4
|
const path = require("path");
|
|
5
|
-
const snapshots = require("./chunks/snapshots-
|
|
5
|
+
const snapshots = require("./chunks/snapshots-CcvHHTnQ.js");
|
|
6
6
|
const crypto = require("crypto");
|
|
7
7
|
const undici = require("undici");
|
|
8
8
|
const os = require("os");
|
|
@@ -602,9 +602,13 @@ async function cmdFuzz(args) {
|
|
|
602
602
|
process.exit(2);
|
|
603
603
|
}
|
|
604
604
|
const includeWrites = Boolean(args["include-writes"]);
|
|
605
|
-
|
|
605
|
+
const levelArg = typeof args["level"] === "string" ? args["level"].toLowerCase() : "standard";
|
|
606
|
+
const level = ["basic", "standard", "aggressive"].includes(levelArg) ? levelArg : "standard";
|
|
607
|
+
console.log(` Fuzzing ${allRequests.length} request(s)${specUrl || specPath ? " against the spec" : " from request bodies"} at the ${level} level...`);
|
|
608
|
+
if (level === "aggressive") console.log(" Aggressive: includes injection probes (SQL/NoSQL/…) and header tampering. Only run against systems you own.");
|
|
606
609
|
if (!includeWrites) console.log(" Write methods (POST/PUT/PATCH/DELETE) are skipped. Add --include-writes to fuzz them (sends malformed writes; target staging or a mock).");
|
|
607
610
|
const report = await snapshots.runFuzz({
|
|
611
|
+
level,
|
|
608
612
|
requests: allRequests,
|
|
609
613
|
envVars,
|
|
610
614
|
collectionVars,
|
|
@@ -931,7 +935,7 @@ async function main() {
|
|
|
931
935
|
api-spector contract record-deployment --workspace <path> --pacticipant <name> --app-version <ver> --env <name>
|
|
932
936
|
api-spector contract environments --workspace <path>
|
|
933
937
|
api-spector contract webhooks --workspace <path> [--test]
|
|
934
|
-
api-spector contract fuzz --workspace <path> --provider-base-url <url> [--snapshot <id> | --spec-url <url>] [--cases <n>] [--seed <n>] [--include-writes] [--trace] [--html <path>]
|
|
938
|
+
api-spector contract fuzz --workspace <path> --provider-base-url <url> [--snapshot <id> | --spec-url <url>] [--level basic|standard|aggressive] [--cases <n>] [--seed <n>] [--include-writes] [--trace] [--html <path>]
|
|
935
939
|
api-spector contract pact-import --file <pact.json> [--out <collection.json>]
|
|
936
940
|
api-spector contract pact-export --workspace <path> --out <pact.json> [--consumer <name> --provider <name> --collection <name>]
|
|
937
941
|
|
package/out/main/index.js
CHANGED
|
@@ -45,7 +45,7 @@ const protoLoader = require("@grpc/proto-loader");
|
|
|
45
45
|
const soapHandler = require("./chunks/soap-handler-DCCE344x.js");
|
|
46
46
|
const promises$1 = require("node:fs/promises");
|
|
47
47
|
const os$1 = require("os");
|
|
48
|
-
const snapshots = require("./chunks/snapshots-
|
|
48
|
+
const snapshots = require("./chunks/snapshots-CcvHHTnQ.js");
|
|
49
49
|
const simpleGit = require("simple-git");
|
|
50
50
|
const recorder = require("./chunks/recorder-0Ij921El.js");
|
|
51
51
|
require("tls");
|
|
@@ -5941,7 +5941,7 @@ function isNewer(a, b) {
|
|
|
5941
5941
|
return a2 > b2;
|
|
5942
5942
|
}
|
|
5943
5943
|
async function checkForUpdate() {
|
|
5944
|
-
const current = "0.5.
|
|
5944
|
+
const current = "0.5.6";
|
|
5945
5945
|
try {
|
|
5946
5946
|
const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(4e3) });
|
|
5947
5947
|
if (!res.ok) return null;
|
package/out/main/runner.js
CHANGED
|
@@ -402,7 +402,7 @@ async function main() {
|
|
|
402
402
|
} else if (!envName && workspace.settings?.defaultEnvironment && !env) {
|
|
403
403
|
console.warn(cliCommon.color(`Warning: default environment "${workspace.settings.defaultEnvironment}" not found. Running without environment.`, cliCommon.C.yellow));
|
|
404
404
|
}
|
|
405
|
-
const version = `v${"0.5.
|
|
405
|
+
const version = `v${"0.5.6"}`;
|
|
406
406
|
console.log("");
|
|
407
407
|
console.log(cliCommon.color(" API Test Runner" + (version ? ` ${version}` : ""), cliCommon.C.bold, cliCommon.C.white));
|
|
408
408
|
console.log(cliCommon.color(` Workspace: ${wsPath}`, cliCommon.C.gray));
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { l as linter, E as EditorView, L as LanguageSupport, a as LRLanguage, s as styleTags, i as indentNodeProp, d as delimitedIndent, f as foldNodeProp, b as foldInside, S as StateField, c as LRParser, e as StateEffect, t as tags, u as useT, r as reactExports, g as useStore, h as useActiveEnvironment, j as jsxRuntimeExports, R as ReactCodeMirror, o as oneDark, k as json } from "./index-
|
|
1
|
+
import { l as linter, E as EditorView, L as LanguageSupport, a as LRLanguage, s as styleTags, i as indentNodeProp, d as delimitedIndent, f as foldNodeProp, b as foldInside, S as StateField, c as LRParser, e as StateEffect, t as tags, u as useT, r as reactExports, g as useStore, h as useActiveEnvironment, j as jsxRuntimeExports, R as ReactCodeMirror, o as oneDark, k as json } from "./index-BKJHj4_d.js";
|
|
2
2
|
function devAssert(condition, message) {
|
|
3
3
|
const booleanCondition = Boolean(condition);
|
|
4
4
|
if (!booleanCondition) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { u as useT, r as reactExports, j as jsxRuntimeExports, R as ReactCodeMirror, m as commentKeymap, o as oneDark, x as xml } from "./index-
|
|
1
|
+
import { u as useT, r as reactExports, j as jsxRuntimeExports, R as ReactCodeMirror, m as commentKeymap, o as oneDark, x as xml } from "./index-BKJHj4_d.js";
|
|
2
2
|
const SOAP_11_CONTENT_TYPE = "text/xml; charset=utf-8";
|
|
3
3
|
const SOAP_12_CONTENT_TYPE = "application/soap+xml; charset=utf-8";
|
|
4
4
|
function contentTypeForSoap(version) {
|