@datasynx/agentic-ai-cartography 2.10.0 → 2.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +142 -2
- package/dist/api-bin.js +2 -2
- package/dist/{chunk-YVV6NIT2.js → chunk-LO6YFS6H.js} +2 -1
- package/dist/{chunk-ASCA3UFM.js → chunk-OIDAXUW5.js} +340 -204
- package/dist/chunk-OIDAXUW5.js.map +1 -0
- package/dist/{chunk-W4Q3TXHR.js → chunk-PD67MOKR.js} +2 -2
- package/dist/cli.js +97 -5
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +241 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +126 -3
- package/dist/index.d.ts +126 -3
- package/dist/index.js +217 -16
- package/dist/index.js.map +1 -1
- package/dist/mcp-bin.js +2 -2
- package/llms-full.txt +305 -25
- package/package.json +1 -1
- package/server.json +2 -2
- package/dist/chunk-ASCA3UFM.js.map +0 -1
- /package/dist/{chunk-YVV6NIT2.js.map → chunk-LO6YFS6H.js.map} +0 -0
- /package/dist/{chunk-W4Q3TXHR.js.map → chunk-PD67MOKR.js.map} +0 -0
|
@@ -20,12 +20,13 @@ import {
|
|
|
20
20
|
k8sScanner,
|
|
21
21
|
keyMetaOf,
|
|
22
22
|
normalizeTenant,
|
|
23
|
+
redactSecrets,
|
|
23
24
|
redactValue,
|
|
24
25
|
resolvePrincipal,
|
|
25
26
|
sanitizeUntrusted,
|
|
26
27
|
stableStringify,
|
|
27
28
|
stripSensitive
|
|
28
|
-
} from "./chunk-
|
|
29
|
+
} from "./chunk-LO6YFS6H.js";
|
|
29
30
|
import {
|
|
30
31
|
EdgeSchema,
|
|
31
32
|
NODE_TYPES,
|
|
@@ -653,9 +654,132 @@ var serviceConfigScanner = {
|
|
|
653
654
|
}
|
|
654
655
|
};
|
|
655
656
|
|
|
657
|
+
// src/scanners/terraform.ts
|
|
658
|
+
import { readFileSync } from "fs";
|
|
659
|
+
var TYPE_RULES = [
|
|
660
|
+
[/(db_instance|_rds|sql_database|sql_instance|database_instance|cosmosdb|dynamodb|spanner|bigtable|documentdb|redshift)/, "database_server"],
|
|
661
|
+
[/(elasticache|_redis|memcached|memorystore)/, "cache_server"],
|
|
662
|
+
[/(s3_bucket|storage_bucket|gcs_bucket|storage_account|_blob)/, "database"],
|
|
663
|
+
[/(_sqs|_queue|servicebus_queue)/, "queue"],
|
|
664
|
+
[/(_sns|_topic|pubsub_topic|servicebus_topic)/, "topic"],
|
|
665
|
+
[/(kafka|_msk|event_hub|kinesis)/, "message_broker"],
|
|
666
|
+
[/(_eks|_gke|_aks|kubernetes_cluster|container_cluster)/, "k8s_cluster"],
|
|
667
|
+
[/(ecs_|_container|fargate)/, "container"],
|
|
668
|
+
[/(lambda|cloud_function|cloudfunctions|function_app|cloud_run)/, "web_service"],
|
|
669
|
+
[/(_lb$|load_balancer|_alb|_elb|application_gateway)/, "web_service"],
|
|
670
|
+
[/(api_gateway|apigateway)/, "api_endpoint"],
|
|
671
|
+
[/(_instance|virtual_machine|_vm$|compute_instance)/, "host"]
|
|
672
|
+
];
|
|
673
|
+
function terraformTypeToNode(tfType) {
|
|
674
|
+
const t = tfType.toLowerCase();
|
|
675
|
+
for (const [re, nt] of TYPE_RULES) if (re.test(t)) return nt;
|
|
676
|
+
return "unknown";
|
|
677
|
+
}
|
|
678
|
+
var IDENTITY_ATTRS = ["id", "arn", "region", "location", "instance_type", "engine", "machine_type"];
|
|
679
|
+
var OWNER_TAGS = ["Owner", "owner", "Team", "team"];
|
|
680
|
+
var SAFE_TAG_KEYS = /* @__PURE__ */ new Set(["Name", "name", "Owner", "owner", "Team", "team", "Env", "env", "Environment", "environment", "Service", "service", "Component", "component", "App", "app", "Project", "project", "Tier", "tier", "Role", "role"]);
|
|
681
|
+
var SECRET_KEY = /pass|secret|token|key|pwd|cred|private/i;
|
|
682
|
+
function attrTags(tags) {
|
|
683
|
+
if (!tags || typeof tags !== "object") return [];
|
|
684
|
+
return Object.entries(tags).filter(([k]) => SAFE_TAG_KEYS.has(k) && !SECRET_KEY.test(k)).map(([k, v]) => `${k}:${redactSecrets(String(v))}`);
|
|
685
|
+
}
|
|
686
|
+
function parseTerraformState(json2) {
|
|
687
|
+
let state;
|
|
688
|
+
try {
|
|
689
|
+
state = JSON.parse(json2);
|
|
690
|
+
} catch {
|
|
691
|
+
return { nodes: [], edges: [] };
|
|
692
|
+
}
|
|
693
|
+
const resources = Array.isArray(state?.resources) ? state.resources : [];
|
|
694
|
+
const nodes = [];
|
|
695
|
+
const edges = [];
|
|
696
|
+
const addrToId = /* @__PURE__ */ new Map();
|
|
697
|
+
for (const raw of resources) {
|
|
698
|
+
const r = raw;
|
|
699
|
+
if (r.mode && r.mode !== "managed") continue;
|
|
700
|
+
if (typeof r.type !== "string" || typeof r.name !== "string") continue;
|
|
701
|
+
const address = `${r.type}.${r.name}`;
|
|
702
|
+
const nt = terraformTypeToNode(r.type);
|
|
703
|
+
const id = `${nt}:terraform:${address}`;
|
|
704
|
+
if (addrToId.has(address)) continue;
|
|
705
|
+
addrToId.set(address, id);
|
|
706
|
+
const inst = Array.isArray(r.instances) ? r.instances[0] : void 0;
|
|
707
|
+
const attrs = inst?.attributes ?? {};
|
|
708
|
+
const identity = { source: "terraform", tfType: r.type };
|
|
709
|
+
for (const k of IDENTITY_ATTRS) if (attrs[k] !== void 0) identity[k] = attrs[k];
|
|
710
|
+
const owner = OWNER_TAGS.map((k) => attrs.tags?.[k]).find((v) => typeof v === "string");
|
|
711
|
+
nodes.push({
|
|
712
|
+
id,
|
|
713
|
+
type: nt,
|
|
714
|
+
name: address,
|
|
715
|
+
discoveredVia: "terraform-state",
|
|
716
|
+
confidence: 0.9,
|
|
717
|
+
// IaC is authoritative declared intent.
|
|
718
|
+
metadata: redactValue(identity),
|
|
719
|
+
tags: attrTags(attrs.tags),
|
|
720
|
+
...owner ? { owner } : {}
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
for (const raw of resources) {
|
|
724
|
+
const r = raw;
|
|
725
|
+
if (r.mode && r.mode !== "managed") continue;
|
|
726
|
+
if (typeof r.type !== "string" || typeof r.name !== "string") continue;
|
|
727
|
+
const srcId = addrToId.get(`${r.type}.${r.name}`);
|
|
728
|
+
if (!srcId) continue;
|
|
729
|
+
const inst = Array.isArray(r.instances) ? r.instances[0] : void 0;
|
|
730
|
+
const deps = Array.isArray(inst?.dependencies) ? inst.dependencies : [];
|
|
731
|
+
for (const dep of deps) {
|
|
732
|
+
if (typeof dep !== "string") continue;
|
|
733
|
+
const tgtId = addrToId.get(dep) ?? addrToId.get(dep.split("[")[0]);
|
|
734
|
+
if (!tgtId || tgtId === srcId) continue;
|
|
735
|
+
edges.push({ sourceId: srcId, targetId: tgtId, relationship: "depends_on", evidence: evidenceLine("config-declared", `terraform depends_on ${dep}`), confidence: 0.85 });
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
return { nodes, edges };
|
|
739
|
+
}
|
|
740
|
+
function stateDirs() {
|
|
741
|
+
return [".", "./terraform", "./infra", "./infrastructure", "./deploy", "./terraform/environments"];
|
|
742
|
+
}
|
|
743
|
+
function hintPath(hint) {
|
|
744
|
+
if (!hint) return void 0;
|
|
745
|
+
const m = /(?:^|[\s,])tfstate=([^\s,]+)/.exec(hint);
|
|
746
|
+
return m ? m[1] : void 0;
|
|
747
|
+
}
|
|
748
|
+
function resolveStatePath(ctx) {
|
|
749
|
+
const explicit = hintPath(ctx.hint);
|
|
750
|
+
if (explicit) return explicit;
|
|
751
|
+
const found = (ctx.findFiles ?? findFiles)(stateDirs(), ["*.tfstate"], 4, 20).split(/\r?\n/).map((s) => s.trim()).filter(Boolean);
|
|
752
|
+
return found[0];
|
|
753
|
+
}
|
|
754
|
+
function readStateFile(path) {
|
|
755
|
+
try {
|
|
756
|
+
return readFileSync(path, "utf8");
|
|
757
|
+
} catch {
|
|
758
|
+
return "";
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
var terraformScanner = {
|
|
762
|
+
id: "terraform-state",
|
|
763
|
+
title: "Terraform state (IaC)",
|
|
764
|
+
platforms: "all",
|
|
765
|
+
// No shell commands — the state file is read directly via node:fs, so an
|
|
766
|
+
// operator-supplied path can never inject a command (no `cat "${path}"` interpolation).
|
|
767
|
+
detect(ctx) {
|
|
768
|
+
return resolveStatePath(ctx) !== void 0;
|
|
769
|
+
},
|
|
770
|
+
async scan(ctx) {
|
|
771
|
+
const path = resolveStatePath(ctx);
|
|
772
|
+
if (!path) return { nodes: [], edges: [] };
|
|
773
|
+
const json2 = (ctx.readFile ?? readStateFile)(path);
|
|
774
|
+
if (!json2) return { nodes: [], edges: [] };
|
|
775
|
+
const result = parseTerraformState(json2);
|
|
776
|
+
return { ...result, report: `terraform-state: ${result.nodes.length} resources, ${result.edges.length} dependencies from ${path}` };
|
|
777
|
+
}
|
|
778
|
+
};
|
|
779
|
+
|
|
656
780
|
// src/scanners/registry.ts
|
|
657
781
|
function defaultRegistry() {
|
|
658
|
-
return new ScannerRegistry().register(bookmarksScanner).register(installedAppsScanner).register(portsScanner).register(cloudAwsScanner).register(cloudGcpScanner).register(cloudAzureScanner).register(k8sScanner).register(databasesScanner).register(connectionsScanner).register(serviceConfigScanner);
|
|
782
|
+
return new ScannerRegistry().register(bookmarksScanner).register(installedAppsScanner).register(portsScanner).register(cloudAwsScanner).register(cloudGcpScanner).register(cloudAzureScanner).register(k8sScanner).register(databasesScanner).register(connectionsScanner).register(serviceConfigScanner).register(terraformScanner);
|
|
659
783
|
}
|
|
660
784
|
|
|
661
785
|
// src/scanners/loader.ts
|
|
@@ -764,201 +888,6 @@ function localDiscoveryFn(registry, plugins) {
|
|
|
764
888
|
};
|
|
765
889
|
}
|
|
766
890
|
|
|
767
|
-
// src/compliance/rulesets/baseline.ts
|
|
768
|
-
var baseline = RulesetSchema.parse({
|
|
769
|
-
name: "baseline",
|
|
770
|
-
version: "1.0.0",
|
|
771
|
-
framework: "baseline",
|
|
772
|
-
description: "Deterministic baseline hygiene controls scored against signals available today.",
|
|
773
|
-
rules: [
|
|
774
|
-
{
|
|
775
|
-
id: "BASE-1",
|
|
776
|
-
control: "BASE-1",
|
|
777
|
-
framework: "baseline",
|
|
778
|
-
severity: "medium",
|
|
779
|
-
title: "Asset has an owner",
|
|
780
|
-
rationale: "Every asset should have a clear owning team/person for accountability.",
|
|
781
|
-
scope: {},
|
|
782
|
-
check: { any: [
|
|
783
|
-
{ field: "owner", op: "present" },
|
|
784
|
-
{ field: "domain", op: "present" },
|
|
785
|
-
{ field: "tags", op: "matches", pattern: "owner_key" },
|
|
786
|
-
{ field: "metadataKeys", op: "matches", pattern: "owner_key" }
|
|
787
|
-
] }
|
|
788
|
-
},
|
|
789
|
-
{
|
|
790
|
-
id: "BASE-2",
|
|
791
|
-
control: "BASE-2",
|
|
792
|
-
framework: "baseline",
|
|
793
|
-
severity: "low",
|
|
794
|
-
title: "Service/data asset is assigned a business domain",
|
|
795
|
-
rationale: "Domain assignment enables blast-radius and ownership analysis.",
|
|
796
|
-
scope: { groups: ["data", "web"] },
|
|
797
|
-
check: { field: "domain", op: "present" }
|
|
798
|
-
},
|
|
799
|
-
{
|
|
800
|
-
id: "BASE-3",
|
|
801
|
-
control: "BASE-3",
|
|
802
|
-
framework: "baseline",
|
|
803
|
-
severity: "high",
|
|
804
|
-
title: "Critical asset is discovered with adequate confidence",
|
|
805
|
-
rationale: "Low-confidence critical assets indicate incomplete or unreliable discovery.",
|
|
806
|
-
scope: { groups: ["data", "infra"] },
|
|
807
|
-
check: { field: "confidence", op: "gte", value: 0.5 }
|
|
808
|
-
},
|
|
809
|
-
{
|
|
810
|
-
id: "BASE-4",
|
|
811
|
-
control: "BASE-4",
|
|
812
|
-
framework: "baseline",
|
|
813
|
-
severity: "critical",
|
|
814
|
-
title: "No embedded credentials / plaintext DSN in metadata",
|
|
815
|
-
rationale: "A connection string with embedded credentials is a direct secret-exposure risk.",
|
|
816
|
-
scope: {},
|
|
817
|
-
check: { not: { field: "metadataValues", op: "matches", pattern: "dsn_with_credentials" } }
|
|
818
|
-
},
|
|
819
|
-
{
|
|
820
|
-
id: "BASE-5",
|
|
821
|
-
control: "BASE-5",
|
|
822
|
-
framework: "baseline",
|
|
823
|
-
severity: "medium",
|
|
824
|
-
title: "Data store carries an acceptable quality score",
|
|
825
|
-
rationale: "Where a quality score exists, a low score flags an under-governed data store.",
|
|
826
|
-
scope: { groups: ["data"] },
|
|
827
|
-
applicableWhen: { field: "qualityScore", op: "present" },
|
|
828
|
-
check: { field: "qualityScore", op: "gte", value: 50 }
|
|
829
|
-
}
|
|
830
|
-
]
|
|
831
|
-
});
|
|
832
|
-
|
|
833
|
-
// src/compliance/rulesets/cis.ts
|
|
834
|
-
var cis = RulesetSchema.parse({
|
|
835
|
-
name: "cis",
|
|
836
|
-
version: "0.1.0",
|
|
837
|
-
framework: "CIS",
|
|
838
|
-
description: "CIS starter subset \u2014 illustrative controls, not a certified benchmark.",
|
|
839
|
-
rules: [
|
|
840
|
-
{
|
|
841
|
-
id: "CIS-1.1",
|
|
842
|
-
control: "CIS-1.1",
|
|
843
|
-
framework: "CIS",
|
|
844
|
-
severity: "critical",
|
|
845
|
-
title: "No plaintext credentials in service configuration",
|
|
846
|
-
rationale: "CIS hardening forbids embedded secrets in config/metadata.",
|
|
847
|
-
scope: {},
|
|
848
|
-
check: { not: { field: "metadataValues", op: "matches", pattern: "dsn_with_credentials" } }
|
|
849
|
-
},
|
|
850
|
-
{
|
|
851
|
-
id: "CIS-2.1",
|
|
852
|
-
control: "CIS-2.1",
|
|
853
|
-
framework: "CIS",
|
|
854
|
-
severity: "high",
|
|
855
|
-
title: "Data store is not publicly exposed",
|
|
856
|
-
rationale: "Data stores should not bind to public/0.0.0.0 addresses.",
|
|
857
|
-
scope: { groups: ["data"] },
|
|
858
|
-
check: { not: { field: "metadataValues", op: "matches", pattern: "public_exposure" } }
|
|
859
|
-
},
|
|
860
|
-
{
|
|
861
|
-
id: "CIS-3.1",
|
|
862
|
-
control: "CIS-3.1",
|
|
863
|
-
framework: "CIS",
|
|
864
|
-
severity: "medium",
|
|
865
|
-
title: "Asset inventory has an accountable owner",
|
|
866
|
-
rationale: "CIS asset management requires an assigned owner.",
|
|
867
|
-
scope: {},
|
|
868
|
-
check: { any: [{ field: "owner", op: "present" }, { field: "metadataKeys", op: "matches", pattern: "owner_key" }] }
|
|
869
|
-
}
|
|
870
|
-
]
|
|
871
|
-
});
|
|
872
|
-
|
|
873
|
-
// src/compliance/rulesets/soc2.ts
|
|
874
|
-
var soc2 = RulesetSchema.parse({
|
|
875
|
-
name: "soc2",
|
|
876
|
-
version: "0.1.0",
|
|
877
|
-
framework: "SOC2",
|
|
878
|
-
description: "SOC 2 starter subset \u2014 illustrative controls, not a certified control set.",
|
|
879
|
-
rules: [
|
|
880
|
-
{
|
|
881
|
-
id: "CC6.1",
|
|
882
|
-
control: "CC6.1",
|
|
883
|
-
framework: "SOC2",
|
|
884
|
-
severity: "critical",
|
|
885
|
-
title: "Logical access \u2014 no embedded credentials",
|
|
886
|
-
rationale: "CC6.1 logical access controls preclude plaintext secrets in config.",
|
|
887
|
-
scope: {},
|
|
888
|
-
check: { not: { field: "metadataValues", op: "matches", pattern: "dsn_with_credentials" } }
|
|
889
|
-
},
|
|
890
|
-
{
|
|
891
|
-
id: "CC6.6",
|
|
892
|
-
control: "CC6.6",
|
|
893
|
-
framework: "SOC2",
|
|
894
|
-
severity: "high",
|
|
895
|
-
title: "Boundary protection \u2014 data stores not public",
|
|
896
|
-
rationale: "CC6.6 requires protection of system boundaries from external access.",
|
|
897
|
-
scope: { groups: ["data"] },
|
|
898
|
-
check: { not: { field: "metadataValues", op: "matches", pattern: "public_exposure" } }
|
|
899
|
-
},
|
|
900
|
-
{
|
|
901
|
-
id: "CC1.2",
|
|
902
|
-
control: "CC1.2",
|
|
903
|
-
framework: "SOC2",
|
|
904
|
-
severity: "medium",
|
|
905
|
-
title: "Accountability \u2014 asset has an owner",
|
|
906
|
-
rationale: "CC1.2 establishes accountability; assets need an assigned owner.",
|
|
907
|
-
scope: {},
|
|
908
|
-
check: { any: [{ field: "owner", op: "present" }, { field: "domain", op: "present" }] }
|
|
909
|
-
}
|
|
910
|
-
]
|
|
911
|
-
});
|
|
912
|
-
|
|
913
|
-
// src/compliance/rulesets/iso27001.ts
|
|
914
|
-
var iso27001 = RulesetSchema.parse({
|
|
915
|
-
name: "iso27001",
|
|
916
|
-
version: "0.1.0",
|
|
917
|
-
framework: "ISO27001",
|
|
918
|
-
description: "ISO/IEC 27001 Annex A starter subset \u2014 illustrative, not certified.",
|
|
919
|
-
rules: [
|
|
920
|
-
{
|
|
921
|
-
id: "A.8.1",
|
|
922
|
-
control: "A.8.1",
|
|
923
|
-
framework: "ISO27001",
|
|
924
|
-
severity: "medium",
|
|
925
|
-
title: "Inventory of assets \u2014 ownership assigned",
|
|
926
|
-
rationale: "A.8.1 requires an inventory of assets each with an identified owner.",
|
|
927
|
-
scope: {},
|
|
928
|
-
check: { any: [{ field: "owner", op: "present" }, { field: "metadataKeys", op: "matches", pattern: "owner_key" }] }
|
|
929
|
-
},
|
|
930
|
-
{
|
|
931
|
-
id: "A.8.12",
|
|
932
|
-
control: "A.8.12",
|
|
933
|
-
framework: "ISO27001",
|
|
934
|
-
severity: "critical",
|
|
935
|
-
title: "Data leakage prevention \u2014 no embedded secrets",
|
|
936
|
-
rationale: "A.8.12 mandates measures against data leakage such as exposed credentials.",
|
|
937
|
-
scope: {},
|
|
938
|
-
check: { not: { field: "metadataValues", op: "matches", pattern: "dsn_with_credentials" } }
|
|
939
|
-
},
|
|
940
|
-
{
|
|
941
|
-
id: "A.8.20",
|
|
942
|
-
control: "A.8.20",
|
|
943
|
-
framework: "ISO27001",
|
|
944
|
-
severity: "high",
|
|
945
|
-
title: "Network security \u2014 data stores not publicly exposed",
|
|
946
|
-
rationale: "A.8.20 requires networks to be secured; data stores should not be public.",
|
|
947
|
-
scope: { groups: ["data"] },
|
|
948
|
-
check: { not: { field: "metadataValues", op: "matches", pattern: "public_exposure" } }
|
|
949
|
-
}
|
|
950
|
-
]
|
|
951
|
-
});
|
|
952
|
-
|
|
953
|
-
// src/compliance/rulesets/registry.ts
|
|
954
|
-
var RULESETS = { baseline, cis, soc2, iso27001 };
|
|
955
|
-
function getRuleset(name) {
|
|
956
|
-
return RULESETS[name];
|
|
957
|
-
}
|
|
958
|
-
function listRulesets() {
|
|
959
|
-
return Object.values(RULESETS).map((r) => ({ name: r.name, version: r.version, framework: r.framework, ruleCount: r.rules.length }));
|
|
960
|
-
}
|
|
961
|
-
|
|
962
891
|
// src/sinks/stdout.ts
|
|
963
892
|
var StdoutSink = class {
|
|
964
893
|
name = "stdout";
|
|
@@ -1315,9 +1244,204 @@ async function runDrift(db, config, opts = {}) {
|
|
|
1315
1244
|
return alert;
|
|
1316
1245
|
}
|
|
1317
1246
|
|
|
1247
|
+
// src/compliance/rulesets/baseline.ts
|
|
1248
|
+
var baseline = RulesetSchema.parse({
|
|
1249
|
+
name: "baseline",
|
|
1250
|
+
version: "1.0.0",
|
|
1251
|
+
framework: "baseline",
|
|
1252
|
+
description: "Deterministic baseline hygiene controls scored against signals available today.",
|
|
1253
|
+
rules: [
|
|
1254
|
+
{
|
|
1255
|
+
id: "BASE-1",
|
|
1256
|
+
control: "BASE-1",
|
|
1257
|
+
framework: "baseline",
|
|
1258
|
+
severity: "medium",
|
|
1259
|
+
title: "Asset has an owner",
|
|
1260
|
+
rationale: "Every asset should have a clear owning team/person for accountability.",
|
|
1261
|
+
scope: {},
|
|
1262
|
+
check: { any: [
|
|
1263
|
+
{ field: "owner", op: "present" },
|
|
1264
|
+
{ field: "domain", op: "present" },
|
|
1265
|
+
{ field: "tags", op: "matches", pattern: "owner_key" },
|
|
1266
|
+
{ field: "metadataKeys", op: "matches", pattern: "owner_key" }
|
|
1267
|
+
] }
|
|
1268
|
+
},
|
|
1269
|
+
{
|
|
1270
|
+
id: "BASE-2",
|
|
1271
|
+
control: "BASE-2",
|
|
1272
|
+
framework: "baseline",
|
|
1273
|
+
severity: "low",
|
|
1274
|
+
title: "Service/data asset is assigned a business domain",
|
|
1275
|
+
rationale: "Domain assignment enables blast-radius and ownership analysis.",
|
|
1276
|
+
scope: { groups: ["data", "web"] },
|
|
1277
|
+
check: { field: "domain", op: "present" }
|
|
1278
|
+
},
|
|
1279
|
+
{
|
|
1280
|
+
id: "BASE-3",
|
|
1281
|
+
control: "BASE-3",
|
|
1282
|
+
framework: "baseline",
|
|
1283
|
+
severity: "high",
|
|
1284
|
+
title: "Critical asset is discovered with adequate confidence",
|
|
1285
|
+
rationale: "Low-confidence critical assets indicate incomplete or unreliable discovery.",
|
|
1286
|
+
scope: { groups: ["data", "infra"] },
|
|
1287
|
+
check: { field: "confidence", op: "gte", value: 0.5 }
|
|
1288
|
+
},
|
|
1289
|
+
{
|
|
1290
|
+
id: "BASE-4",
|
|
1291
|
+
control: "BASE-4",
|
|
1292
|
+
framework: "baseline",
|
|
1293
|
+
severity: "critical",
|
|
1294
|
+
title: "No embedded credentials / plaintext DSN in metadata",
|
|
1295
|
+
rationale: "A connection string with embedded credentials is a direct secret-exposure risk.",
|
|
1296
|
+
scope: {},
|
|
1297
|
+
check: { not: { field: "metadataValues", op: "matches", pattern: "dsn_with_credentials" } }
|
|
1298
|
+
},
|
|
1299
|
+
{
|
|
1300
|
+
id: "BASE-5",
|
|
1301
|
+
control: "BASE-5",
|
|
1302
|
+
framework: "baseline",
|
|
1303
|
+
severity: "medium",
|
|
1304
|
+
title: "Data store carries an acceptable quality score",
|
|
1305
|
+
rationale: "Where a quality score exists, a low score flags an under-governed data store.",
|
|
1306
|
+
scope: { groups: ["data"] },
|
|
1307
|
+
applicableWhen: { field: "qualityScore", op: "present" },
|
|
1308
|
+
check: { field: "qualityScore", op: "gte", value: 50 }
|
|
1309
|
+
}
|
|
1310
|
+
]
|
|
1311
|
+
});
|
|
1312
|
+
|
|
1313
|
+
// src/compliance/rulesets/cis.ts
|
|
1314
|
+
var cis = RulesetSchema.parse({
|
|
1315
|
+
name: "cis",
|
|
1316
|
+
version: "0.1.0",
|
|
1317
|
+
framework: "CIS",
|
|
1318
|
+
description: "CIS starter subset \u2014 illustrative controls, not a certified benchmark.",
|
|
1319
|
+
rules: [
|
|
1320
|
+
{
|
|
1321
|
+
id: "CIS-1.1",
|
|
1322
|
+
control: "CIS-1.1",
|
|
1323
|
+
framework: "CIS",
|
|
1324
|
+
severity: "critical",
|
|
1325
|
+
title: "No plaintext credentials in service configuration",
|
|
1326
|
+
rationale: "CIS hardening forbids embedded secrets in config/metadata.",
|
|
1327
|
+
scope: {},
|
|
1328
|
+
check: { not: { field: "metadataValues", op: "matches", pattern: "dsn_with_credentials" } }
|
|
1329
|
+
},
|
|
1330
|
+
{
|
|
1331
|
+
id: "CIS-2.1",
|
|
1332
|
+
control: "CIS-2.1",
|
|
1333
|
+
framework: "CIS",
|
|
1334
|
+
severity: "high",
|
|
1335
|
+
title: "Data store is not publicly exposed",
|
|
1336
|
+
rationale: "Data stores should not bind to public/0.0.0.0 addresses.",
|
|
1337
|
+
scope: { groups: ["data"] },
|
|
1338
|
+
check: { not: { field: "metadataValues", op: "matches", pattern: "public_exposure" } }
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
id: "CIS-3.1",
|
|
1342
|
+
control: "CIS-3.1",
|
|
1343
|
+
framework: "CIS",
|
|
1344
|
+
severity: "medium",
|
|
1345
|
+
title: "Asset inventory has an accountable owner",
|
|
1346
|
+
rationale: "CIS asset management requires an assigned owner.",
|
|
1347
|
+
scope: {},
|
|
1348
|
+
check: { any: [{ field: "owner", op: "present" }, { field: "metadataKeys", op: "matches", pattern: "owner_key" }] }
|
|
1349
|
+
}
|
|
1350
|
+
]
|
|
1351
|
+
});
|
|
1352
|
+
|
|
1353
|
+
// src/compliance/rulesets/soc2.ts
|
|
1354
|
+
var soc2 = RulesetSchema.parse({
|
|
1355
|
+
name: "soc2",
|
|
1356
|
+
version: "0.1.0",
|
|
1357
|
+
framework: "SOC2",
|
|
1358
|
+
description: "SOC 2 starter subset \u2014 illustrative controls, not a certified control set.",
|
|
1359
|
+
rules: [
|
|
1360
|
+
{
|
|
1361
|
+
id: "CC6.1",
|
|
1362
|
+
control: "CC6.1",
|
|
1363
|
+
framework: "SOC2",
|
|
1364
|
+
severity: "critical",
|
|
1365
|
+
title: "Logical access \u2014 no embedded credentials",
|
|
1366
|
+
rationale: "CC6.1 logical access controls preclude plaintext secrets in config.",
|
|
1367
|
+
scope: {},
|
|
1368
|
+
check: { not: { field: "metadataValues", op: "matches", pattern: "dsn_with_credentials" } }
|
|
1369
|
+
},
|
|
1370
|
+
{
|
|
1371
|
+
id: "CC6.6",
|
|
1372
|
+
control: "CC6.6",
|
|
1373
|
+
framework: "SOC2",
|
|
1374
|
+
severity: "high",
|
|
1375
|
+
title: "Boundary protection \u2014 data stores not public",
|
|
1376
|
+
rationale: "CC6.6 requires protection of system boundaries from external access.",
|
|
1377
|
+
scope: { groups: ["data"] },
|
|
1378
|
+
check: { not: { field: "metadataValues", op: "matches", pattern: "public_exposure" } }
|
|
1379
|
+
},
|
|
1380
|
+
{
|
|
1381
|
+
id: "CC1.2",
|
|
1382
|
+
control: "CC1.2",
|
|
1383
|
+
framework: "SOC2",
|
|
1384
|
+
severity: "medium",
|
|
1385
|
+
title: "Accountability \u2014 asset has an owner",
|
|
1386
|
+
rationale: "CC1.2 establishes accountability; assets need an assigned owner.",
|
|
1387
|
+
scope: {},
|
|
1388
|
+
check: { any: [{ field: "owner", op: "present" }, { field: "domain", op: "present" }] }
|
|
1389
|
+
}
|
|
1390
|
+
]
|
|
1391
|
+
});
|
|
1392
|
+
|
|
1393
|
+
// src/compliance/rulesets/iso27001.ts
|
|
1394
|
+
var iso27001 = RulesetSchema.parse({
|
|
1395
|
+
name: "iso27001",
|
|
1396
|
+
version: "0.1.0",
|
|
1397
|
+
framework: "ISO27001",
|
|
1398
|
+
description: "ISO/IEC 27001 Annex A starter subset \u2014 illustrative, not certified.",
|
|
1399
|
+
rules: [
|
|
1400
|
+
{
|
|
1401
|
+
id: "A.8.1",
|
|
1402
|
+
control: "A.8.1",
|
|
1403
|
+
framework: "ISO27001",
|
|
1404
|
+
severity: "medium",
|
|
1405
|
+
title: "Inventory of assets \u2014 ownership assigned",
|
|
1406
|
+
rationale: "A.8.1 requires an inventory of assets each with an identified owner.",
|
|
1407
|
+
scope: {},
|
|
1408
|
+
check: { any: [{ field: "owner", op: "present" }, { field: "metadataKeys", op: "matches", pattern: "owner_key" }] }
|
|
1409
|
+
},
|
|
1410
|
+
{
|
|
1411
|
+
id: "A.8.12",
|
|
1412
|
+
control: "A.8.12",
|
|
1413
|
+
framework: "ISO27001",
|
|
1414
|
+
severity: "critical",
|
|
1415
|
+
title: "Data leakage prevention \u2014 no embedded secrets",
|
|
1416
|
+
rationale: "A.8.12 mandates measures against data leakage such as exposed credentials.",
|
|
1417
|
+
scope: {},
|
|
1418
|
+
check: { not: { field: "metadataValues", op: "matches", pattern: "dsn_with_credentials" } }
|
|
1419
|
+
},
|
|
1420
|
+
{
|
|
1421
|
+
id: "A.8.20",
|
|
1422
|
+
control: "A.8.20",
|
|
1423
|
+
framework: "ISO27001",
|
|
1424
|
+
severity: "high",
|
|
1425
|
+
title: "Network security \u2014 data stores not publicly exposed",
|
|
1426
|
+
rationale: "A.8.20 requires networks to be secured; data stores should not be public.",
|
|
1427
|
+
scope: { groups: ["data"] },
|
|
1428
|
+
check: { not: { field: "metadataValues", op: "matches", pattern: "public_exposure" } }
|
|
1429
|
+
}
|
|
1430
|
+
]
|
|
1431
|
+
});
|
|
1432
|
+
|
|
1433
|
+
// src/compliance/rulesets/registry.ts
|
|
1434
|
+
var RULESETS = { baseline, cis, soc2, iso27001 };
|
|
1435
|
+
function getRuleset(name) {
|
|
1436
|
+
return RULESETS[name];
|
|
1437
|
+
}
|
|
1438
|
+
function listRulesets() {
|
|
1439
|
+
return Object.values(RULESETS).map((r) => ({ name: r.name, version: r.version, framework: r.framework, ruleCount: r.rules.length }));
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1318
1442
|
// src/orgkey.ts
|
|
1319
1443
|
import { randomBytes, hkdfSync } from "crypto";
|
|
1320
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync, statSync } from "fs";
|
|
1444
|
+
import { existsSync, mkdirSync, readFileSync as readFileSync2, writeFileSync, statSync } from "fs";
|
|
1321
1445
|
import { homedir } from "os";
|
|
1322
1446
|
import { dirname, join } from "path";
|
|
1323
1447
|
function orgKeyPath(home = homedir()) {
|
|
@@ -1333,7 +1457,7 @@ function loadFileSecret(path) {
|
|
|
1333
1457
|
}
|
|
1334
1458
|
} catch {
|
|
1335
1459
|
}
|
|
1336
|
-
const hex =
|
|
1460
|
+
const hex = readFileSync2(path, "utf8").trim();
|
|
1337
1461
|
const buf = Buffer.from(hex, "hex");
|
|
1338
1462
|
if (buf.length === KEY_BYTES) return buf;
|
|
1339
1463
|
logWarn("org-key file was malformed \u2014 regenerating a fresh org key");
|
|
@@ -1370,6 +1494,8 @@ function reversalKey(orgKey) {
|
|
|
1370
1494
|
import { createHmac, createCipheriv, createDecipheriv, randomBytes as randomBytes2 } from "crypto";
|
|
1371
1495
|
var PRIVATE_IP = /\b(?:10(?:\.\d{1,3}){3}|192\.168(?:\.\d{1,3}){2}|172\.(?:1[6-9]|2\d|3[01])(?:\.\d{1,3}){2})\b/g;
|
|
1372
1496
|
var HOSTNAME = /\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}\b/gi;
|
|
1497
|
+
var BARE_INTERNAL_HOST = /^[a-z0-9]+(?:-[a-z0-9]+)+$|^[a-z]+\d+$|^\d+[a-z]+$/i;
|
|
1498
|
+
var ANON_TOKEN = /^anon:(?:host|user|path|ip):[a-z2-7]+$/;
|
|
1373
1499
|
var POSIX_PATH = /(?:^|(?<=\s|=|:|"|'|\())(\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+)/g;
|
|
1374
1500
|
var WIN_PATH = /\b[A-Za-z]:\\[A-Za-z0-9._\\-]+/g;
|
|
1375
1501
|
var B32_ALPHABET = "abcdefghijklmnopqrstuvwxyz234567";
|
|
@@ -1424,8 +1550,18 @@ function pseudonymizeString(s, orgKey, db) {
|
|
|
1424
1550
|
(_m, user, host) => `${pseudonymizeFragment(user, "user", orgKey, db)}@${pseudonymizeFragment(host, "host", orgKey, db)}`
|
|
1425
1551
|
);
|
|
1426
1552
|
out = out.replace(HOSTNAME, (m) => pseudonymizeFragment(m, "host", orgKey, db));
|
|
1553
|
+
const trimmed = out.trim();
|
|
1554
|
+
if (out === s && !ANON_TOKEN.test(trimmed) && BARE_INTERNAL_HOST.test(trimmed)) {
|
|
1555
|
+
out = pseudonymizeFragment(trimmed, "host", orgKey, db);
|
|
1556
|
+
}
|
|
1427
1557
|
return out;
|
|
1428
1558
|
}
|
|
1559
|
+
function pseudonymizeId(id, orgKey, db) {
|
|
1560
|
+
const segments = id.split(":");
|
|
1561
|
+
if (segments.length <= 1) return pseudonymizeString(id, orgKey, db);
|
|
1562
|
+
const [type, ...rest] = segments;
|
|
1563
|
+
return [type, ...rest.map((seg) => pseudonymizeString(seg, orgKey, db))].join(":");
|
|
1564
|
+
}
|
|
1429
1565
|
function pseudonymize(value, orgKey, db) {
|
|
1430
1566
|
if (typeof value === "string") return pseudonymizeString(value, orgKey, db);
|
|
1431
1567
|
if (Array.isArray(value)) return value.map((v) => pseudonymize(v, orgKey, db));
|
|
@@ -1676,7 +1812,7 @@ function correlateTopology(nodes, _edges = []) {
|
|
|
1676
1812
|
|
|
1677
1813
|
// src/mcp/server.ts
|
|
1678
1814
|
var SERVER_NAME = "cartography";
|
|
1679
|
-
var SERVER_VERSION = "2.
|
|
1815
|
+
var SERVER_VERSION = "2.12.1";
|
|
1680
1816
|
var SERVICE_TYPES = NODE_TYPE_GROUPS.web;
|
|
1681
1817
|
var DATA_TYPES = NODE_TYPE_GROUPS.data;
|
|
1682
1818
|
var lexicalSearch = async (db, sessionId, query, opts) => db.searchNodes(sessionId, query, { types: opts.types, limit: opts.limit }).map((node) => ({ node }));
|
|
@@ -2688,8 +2824,6 @@ var FQDN = /\b(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,}\b/gi;
|
|
|
2688
2824
|
var POSIX_PATH2 = /(?:^|(?<=\s|=|:|"|'|\())(\/[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)+)/g;
|
|
2689
2825
|
var WIN_PATH2 = /\b[A-Za-z]:\\[A-Za-z0-9._\\-]+/g;
|
|
2690
2826
|
var HOME_USER = /(?:\/home\/|\/Users\/|[A-Za-z]:\\Users\\)([A-Za-z0-9._-]+)/g;
|
|
2691
|
-
var BARE_INTERNAL_HOST = /^[a-z0-9]+(?:-[a-z0-9]+)+$|^[a-z]+\d+$|^\d+[a-z]+$/i;
|
|
2692
|
-
var ANON_TOKEN = /^anon:(?:host|user|path|ip):[a-z2-7]+$/;
|
|
2693
2827
|
function violationsInString(s, path) {
|
|
2694
2828
|
const out = [];
|
|
2695
2829
|
const trimmed = s.trim();
|
|
@@ -3015,17 +3149,19 @@ async function startMcp(opts = {}) {
|
|
|
3015
3149
|
}
|
|
3016
3150
|
|
|
3017
3151
|
export {
|
|
3152
|
+
ScannerRegistry,
|
|
3018
3153
|
isPersonalHost,
|
|
3019
3154
|
runLocalDiscovery,
|
|
3155
|
+
runDrift,
|
|
3020
3156
|
getRuleset,
|
|
3021
3157
|
listRulesets,
|
|
3022
|
-
runDrift,
|
|
3023
3158
|
loadOrgKey,
|
|
3024
3159
|
rotateOrgKey,
|
|
3025
3160
|
pseudonymizeString,
|
|
3161
|
+
pseudonymizeId,
|
|
3026
3162
|
pseudonymize,
|
|
3027
3163
|
reversePseudonym,
|
|
3028
3164
|
parseMcpArgs,
|
|
3029
3165
|
startMcp
|
|
3030
3166
|
};
|
|
3031
|
-
//# sourceMappingURL=chunk-
|
|
3167
|
+
//# sourceMappingURL=chunk-OIDAXUW5.js.map
|