@aaa-sentinel/iac-cost 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +31 -7
- package/dist/cli.js +642 -63
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -139,8 +139,32 @@ or account ids. The JSON output does not: it is the complete evidence (see `--fo
|
|
|
139
139
|
`provider` block rather than the resource, so `terraform show -json` often leaves it unknown.
|
|
140
140
|
We say we do not know rather than substituting another region's price.
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
- **Usage is not guessed.** Only what the plan fixes is priced: the instance hours (730 a month)
|
|
143
|
+
and, for RDS, the allocated storage plus the IOPS and throughput provisioned above what the
|
|
144
|
+
volume includes. Usage-based charges — data transfer, backups beyond the allocated size, I/O
|
|
145
|
+
requests, CPU credits of burstable classes — are not added, nor storage growth from autoscaling
|
|
146
|
+
(`max_allocated_storage`), and neither are RDS Extended
|
|
147
|
+
Support (engine versions past standard support) or paid Performance Insights retention.
|
|
148
|
+
- **Every price-moving attribute must be known.** An `aws_db_instance` is priced only when the
|
|
149
|
+
plan resolves its engine, Multi-AZ, allocated storage, storage type (and IOPS/throughput where
|
|
150
|
+
they are billed, and the license model for Oracle SE2). An attribute you leave out of the
|
|
151
|
+
configuration shows as "known after apply"; the CLI reads the plan's `configuration` (modules,
|
|
152
|
+
`count` and `for_each` included) and, only when the attribute is not written at all, the AWS
|
|
153
|
+
default is applied: `multi_az` (optional in the RDS API, which creates a Single-AZ instance when
|
|
154
|
+
it is not sent), `storage_type` (`io1` with `iops`, otherwise `gp3`
|
|
155
|
+
per the RDS API; because the provider docs still say `gp2`, it is priced only where gp2 and gp3
|
|
156
|
+
cost the same) and the included gp3 baseline IOPS/throughput. Such rows are marked † in the
|
|
157
|
+
comment with the attributes assumed. Nothing is assumed on updates, when the instance is
|
|
158
|
+
restored from a snapshot or replica (its storage comes from the source), or when `iops` is an
|
|
159
|
+
expression. An attribute written as an expression that is only known after apply is never
|
|
160
|
+
guessed.
|
|
161
|
+
|
|
162
|
+
Currently priced: **AWS** (EC2 on demand and RDS `aws_db_instance`: MySQL, PostgreSQL, MariaDB,
|
|
163
|
+
Oracle and SQL Server, Single-AZ or Multi-AZ, gp2/gp3/io1/io2) and **Azure** (virtual machines).
|
|
164
|
+
Aurora (`aws_rds_cluster`, `aws_rds_cluster_instance`) is not priced: its instance price depends on
|
|
165
|
+
the cluster's storage configuration and its storage and I/O are billed by use. Anything else is
|
|
166
|
+
reported as not priced, never as free. Versions of the CLI before 0.5.0 do not send the RDS
|
|
167
|
+
storage attributes, so their RDS instances stay not priced.
|
|
144
168
|
|
|
145
169
|
## Security before deploy
|
|
146
170
|
|
|
@@ -175,19 +199,19 @@ value from the plan: a variable is named (`DB_PASSWORD`), its value is not read.
|
|
|
175
199
|
|
|
176
200
|
| Cloud | Terraform resources | Rules |
|
|
177
201
|
| --- | --- | --- |
|
|
178
|
-
| AWS | `aws_security_group`, `aws_default_security_group`, `aws_security_group_rule`, `aws_vpc_security_group_ingress_rule` (+ `aws_ec2_managed_prefix_list`) | Admin ports (22/3389) open to the internet, directly or through a prefix list |
|
|
202
|
+
| AWS | `aws_security_group`, `aws_default_security_group`, `aws_security_group_rule`, `aws_vpc_security_group_ingress_rule` (+ `aws_ec2_managed_prefix_list`) | Admin ports (22/3389) open to the internet, directly or through a prefix list; database ports (SQL Server, Oracle, MySQL, PostgreSQL, Redis, Elasticsearch, MongoDB) open to the internet over TCP |
|
|
179
203
|
| AWS | `aws_s3_bucket` + `aws_s3_bucket_public_access_block`, `aws_s3_account_public_access_block`, `aws_s3_bucket_acl`, `aws_s3_bucket_policy` | Public bucket |
|
|
180
|
-
| AWS | `aws_db_instance`, `aws_rds_cluster_instance` (+ `aws_rds_cluster`) | Unencrypted storage |
|
|
204
|
+
| AWS | `aws_db_instance`, `aws_rds_cluster_instance` (+ `aws_rds_cluster`) | Unencrypted storage; publicly accessible |
|
|
181
205
|
| AWS | `aws_lambda_function` + `aws_lambda_function_url`, `aws_lambda_permission` | Invocable from the internet; secret-looking environment variable names |
|
|
182
206
|
| AWS | `aws_cloudtrail` | Trail stopped; management events excluded |
|
|
183
207
|
| AWS | `aws_kms_key` | Key rotation off |
|
|
184
208
|
| AWS | `aws_vpc`, `aws_default_vpc` + `aws_flow_log` (+ `aws_subnet`) | VPC without flow logs; flow logs that miss part of the traffic |
|
|
185
|
-
| Azure | `azurerm_network_security_group`, `azurerm_network_security_rule` | Admin ports open to the internet |
|
|
209
|
+
| Azure | `azurerm_network_security_group`, `azurerm_network_security_rule` | Admin ports open to the internet; database ports open to the internet over TCP |
|
|
186
210
|
| Azure | `azurerm_storage_account` | Public blobs allowed; weak TLS or HTTP |
|
|
187
211
|
| Azure | `azurerm_mssql_server` + `azurerm_mssql_firewall_rule` | Public network access open to 0.0.0.0–255.255.255.255; weak TLS |
|
|
188
212
|
| Azure | `azurerm_role_assignment` | Owner or User Access Administrator at any scope; Contributor at subscription scope (a Contributor at a management group or at `/` is not flagged, neither here nor in the live scan) |
|
|
189
|
-
| Google Cloud | `google_compute_firewall` | Admin ports open to the internet |
|
|
190
|
-
| Google Cloud | `google_sql_database_instance` | Authorized from 0.0.0.0/0; no CMEK |
|
|
213
|
+
| Google Cloud | `google_compute_firewall` | Admin ports open to the internet; database ports open to the internet over TCP |
|
|
214
|
+
| Google Cloud | `google_sql_database_instance` | Authorized from 0.0.0.0/0; no CMEK; unencrypted connections accepted (`ssl_mode`; not judged while the provider still has to compute it) |
|
|
191
215
|
| Google Cloud | `google_cloudfunctions_function`, `google_cloudfunctions2_function` | Secret-looking environment variable names |
|
|
192
216
|
| Google Cloud | `google_project_iam_member`, `_binding`, `_policy`; `google_service_account_key` | Primitive Owner/Editor role; user-managed key |
|
|
193
217
|
|
package/dist/cli.js
CHANGED
|
@@ -2351,18 +2351,18 @@ var $ZodArray = /* @__PURE__ */ $constructor("$ZodArray", (inst, def) => {
|
|
|
2351
2351
|
capSize(inst, arraySize);
|
|
2352
2352
|
});
|
|
2353
2353
|
function handlePropertyResult(result, final, key, input, optin, optout) {
|
|
2354
|
-
const
|
|
2354
|
+
const isPresent2 = key in input;
|
|
2355
2355
|
const isOptionalOut = optout === "optional";
|
|
2356
|
-
if (!
|
|
2356
|
+
if (!isPresent2 && isOptionalOut && optin === "optional") {
|
|
2357
2357
|
return;
|
|
2358
2358
|
}
|
|
2359
2359
|
if (result.issues.length) {
|
|
2360
|
-
if (optin !== void 0 && isOptionalOut && !
|
|
2360
|
+
if (optin !== void 0 && isOptionalOut && !isPresent2) {
|
|
2361
2361
|
return;
|
|
2362
2362
|
}
|
|
2363
2363
|
final.issues.push(...prefixIssues(key, result.issues));
|
|
2364
2364
|
}
|
|
2365
|
-
if (!
|
|
2365
|
+
if (!isPresent2 && optin === void 0) {
|
|
2366
2366
|
if (!result.issues.length) {
|
|
2367
2367
|
final.issues.push({
|
|
2368
2368
|
code: "invalid_type",
|
|
@@ -2374,7 +2374,7 @@ function handlePropertyResult(result, final, key, input, optin, optout) {
|
|
|
2374
2374
|
return;
|
|
2375
2375
|
}
|
|
2376
2376
|
if (result.value === void 0) {
|
|
2377
|
-
if (
|
|
2377
|
+
if (isPresent2 || optin === "defaulted" && !isOptionalOut) {
|
|
2378
2378
|
final.value[key] = void 0;
|
|
2379
2379
|
}
|
|
2380
2380
|
} else {
|
|
@@ -2563,7 +2563,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
2563
2563
|
continue;
|
|
2564
2564
|
const id = ids[key];
|
|
2565
2565
|
const k = typeof key === "symbol" ? `syms[${syms.indexOf(key)}]` : esc(key);
|
|
2566
|
-
const
|
|
2566
|
+
const isPresent2 = `${k} in input`;
|
|
2567
2567
|
const schema = shape[key];
|
|
2568
2568
|
const optin = schema?._zod?.optin;
|
|
2569
2569
|
const isOptionalIn = optin !== void 0;
|
|
@@ -2572,7 +2572,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
2572
2572
|
if (isOptionalIn && isOptionalOut) {
|
|
2573
2573
|
const assign = optin === "optional" ? `${id}_present` : `${id}.value !== undefined || ${id}_present`;
|
|
2574
2574
|
doc.write(`
|
|
2575
|
-
const ${id}_present = ${
|
|
2575
|
+
const ${id}_present = ${isPresent2};
|
|
2576
2576
|
if (!${id}.issues.length || ${id}_present) {
|
|
2577
2577
|
if (${id}.issues.length) {${prefixStr(id, k)}
|
|
2578
2578
|
}
|
|
@@ -2585,7 +2585,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
2585
2585
|
`);
|
|
2586
2586
|
} else if (!isOptionalIn) {
|
|
2587
2587
|
doc.write(`
|
|
2588
|
-
const ${id}_present = ${
|
|
2588
|
+
const ${id}_present = ${isPresent2};
|
|
2589
2589
|
if (${id}.issues.length) {${prefixStr(id, k)}
|
|
2590
2590
|
}
|
|
2591
2591
|
if (!${id}_present && !${id}.issues.length) {
|
|
@@ -2615,7 +2615,7 @@ var $ZodObjectJIT = /* @__PURE__ */ $constructor("$ZodObjectJIT", (inst, def) =>
|
|
|
2615
2615
|
doc.write(`newResult[${k}] = ${id}.value;`);
|
|
2616
2616
|
} else {
|
|
2617
2617
|
doc.write(`
|
|
2618
|
-
if (${id}.value !== undefined || ${
|
|
2618
|
+
if (${id}.value !== undefined || ${isPresent2}) {
|
|
2619
2619
|
newResult[${k}] = ${id}.value;
|
|
2620
2620
|
}
|
|
2621
2621
|
`);
|
|
@@ -6265,6 +6265,8 @@ function iacProviderOf(resourceType) {
|
|
|
6265
6265
|
billing: !NON_BILLING_PROVIDERS.has(name)
|
|
6266
6266
|
};
|
|
6267
6267
|
}
|
|
6268
|
+
var IAC_UNSET_IN_CONFIG = "unset_in_config";
|
|
6269
|
+
var IAC_UNKNOWN_IN_PLAN = "unknown_in_plan";
|
|
6268
6270
|
var IAC_RESOURCE_SPECS = {
|
|
6269
6271
|
// ── Azure ──
|
|
6270
6272
|
azurerm_linux_virtual_machine: {
|
|
@@ -6295,16 +6297,35 @@ var IAC_RESOURCE_SPECS = {
|
|
|
6295
6297
|
instancesAttribute: "instances",
|
|
6296
6298
|
os: "windows"
|
|
6297
6299
|
},
|
|
6298
|
-
// ── AWS
|
|
6300
|
+
// ── AWS ──
|
|
6301
|
+
// hashicorp/aws 6.x admite `region` por recurso; sin escribirla el plan trae la del provider.
|
|
6299
6302
|
aws_instance: {
|
|
6300
6303
|
provider: "aws",
|
|
6301
|
-
attributes: ["instance_type"],
|
|
6302
|
-
sizeAttribute: "instance_type"
|
|
6304
|
+
attributes: ["instance_type", "region"],
|
|
6305
|
+
sizeAttribute: "instance_type",
|
|
6306
|
+
regionAttribute: "region",
|
|
6307
|
+
regionPlannedByProvider: true
|
|
6303
6308
|
},
|
|
6309
|
+
// Aurora (`aws_rds_cluster*`) no está: el precio de su instancia depende del `storage_type` del clúster, que es otro recurso.
|
|
6304
6310
|
aws_db_instance: {
|
|
6305
6311
|
provider: "aws",
|
|
6306
|
-
attributes: [
|
|
6307
|
-
|
|
6312
|
+
attributes: [
|
|
6313
|
+
"instance_class",
|
|
6314
|
+
"engine",
|
|
6315
|
+
"license_model",
|
|
6316
|
+
"multi_az",
|
|
6317
|
+
"allocated_storage",
|
|
6318
|
+
"storage_type",
|
|
6319
|
+
"iops",
|
|
6320
|
+
"storage_throughput",
|
|
6321
|
+
"region"
|
|
6322
|
+
],
|
|
6323
|
+
sizeAttribute: "instance_class",
|
|
6324
|
+
regionAttribute: "region",
|
|
6325
|
+
regionPlannedByProvider: true,
|
|
6326
|
+
unsetReportable: ["multi_az", "storage_type", "iops", "storage_throughput"],
|
|
6327
|
+
unsetBlockedBy: ["snapshot_identifier", "replicate_source_db"],
|
|
6328
|
+
unsetBlockedByBlocks: ["restore_to_point_in_time", "s3_import"]
|
|
6308
6329
|
},
|
|
6309
6330
|
// ── GCP: ídem ──
|
|
6310
6331
|
google_compute_instance: {
|
|
@@ -6378,7 +6399,7 @@ function aggregateEstimates(estimates) {
|
|
|
6378
6399
|
const pricedProviders = /* @__PURE__ */ new Set();
|
|
6379
6400
|
for (const estimate of estimates) {
|
|
6380
6401
|
for (const item of estimate.priced) {
|
|
6381
|
-
const key = keyOf(item)
|
|
6402
|
+
const key = `${keyOf(item)}#${(item.assumedDefaults ?? []).join(",")}`;
|
|
6382
6403
|
const found = priced.get(key);
|
|
6383
6404
|
if (!found) priced.set(key, { ...item });
|
|
6384
6405
|
else {
|
|
@@ -8153,6 +8174,8 @@ function sqlInstances(index) {
|
|
|
8153
8174
|
const builder = new ObservationBuilder(index, sql, "cloud_sql_instance", "gcp").cloudId("name");
|
|
8154
8175
|
const ip = ["settings", 0, "ip_configuration", 0];
|
|
8155
8176
|
builder.from("ipv4Enabled", index.get(sql, ...ip, "ipv4_enabled"), Boolean, true);
|
|
8177
|
+
builder.from("sslMode", index.get(sql, ...ip, "ssl_mode"), String, null);
|
|
8178
|
+
builder.from("requireSsl", index.get(sql, ...ip, "require_ssl"), Boolean, null);
|
|
8156
8179
|
const networks = index.get(sql, ...ip, "authorized_networks");
|
|
8157
8180
|
if (networks.state === "unknown") builder.gap("authorizedNetworks", "known_after_apply");
|
|
8158
8181
|
else {
|
|
@@ -8268,6 +8291,18 @@ var RULE_TITLES = {
|
|
|
8268
8291
|
SEC_AWS_NETWORK_ADMIN_PREFIX_LIST: "Admin ports open to the internet through a prefix list",
|
|
8269
8292
|
SEC_AWS_EXPOSURE_S3_PUBLICO: "Public S3 bucket",
|
|
8270
8293
|
SEC_AWS_EXPOSURE_LAMBDA_PUBLICA: "Lambda invocable from the internet",
|
|
8294
|
+
SEC_AZURE_ENCRYPTION_KEYVAULT_SIN_PURGA: "Key Vault without purge protection",
|
|
8295
|
+
SEC_AZURE_ENCRYPTION_SQL_SIN_TDE: "SQL database without TDE encryption",
|
|
8296
|
+
SEC_AZURE_LOGGING_VNET_SIN_FLOW_LOGS: "VNet without flow logs",
|
|
8297
|
+
SEC_GCP_ENCRYPTION_KMS_SIN_ROTACION: "Cloud KMS key without 90-day rotation",
|
|
8298
|
+
SEC_GCP_LOGGING_VPC_SIN_FLOW_LOGS: "VPC network with subnets without flow logs",
|
|
8299
|
+
SEC_AWS_NETWORK_BD_ABIERTO: "Database ports open to the internet",
|
|
8300
|
+
SEC_AZURE_NETWORK_BD_ABIERTO: "NSG with database ports open to the internet",
|
|
8301
|
+
SEC_GCP_NETWORK_BD_ABIERTO: "Firewall with database ports open to the internet",
|
|
8302
|
+
SEC_AWS_EXPOSURE_RDS_PUBLICA: "Publicly accessible RDS instance",
|
|
8303
|
+
SEC_GCP_ENCRYPTION_SQL_SIN_SSL: "Cloud SQL accepts unencrypted connections",
|
|
8304
|
+
SEC_AWS_IAM_CLAVE_ANTIGUA: "Active access key older than 90 days",
|
|
8305
|
+
SEC_AWS_IAM_POLITICA_CONTRASENAS: "Weak or missing IAM password policy",
|
|
8271
8306
|
SEC_AWS_ENCRYPTION_KMS_SIN_ROTACION: "KMS key without rotation",
|
|
8272
8307
|
SEC_AWS_ENCRYPTION_RDS_SIN_CIFRAR: "Unencrypted RDS",
|
|
8273
8308
|
SEC_AWS_LOGGING_TRAIL_PARADO: "CloudTrail stopped",
|
|
@@ -9269,6 +9304,62 @@ function extractDefaultRegions(plan) {
|
|
|
9269
9304
|
}
|
|
9270
9305
|
return Object.keys(out).length > 0 ? out : void 0;
|
|
9271
9306
|
}
|
|
9307
|
+
function moduleNames(moduleAddress) {
|
|
9308
|
+
if (!moduleAddress) return [];
|
|
9309
|
+
const parts = moduleAddress.replace(/\[(?:\d+|"(?:[^"\\]|\\.)*")\]/g, "").split(".");
|
|
9310
|
+
if (parts.length % 2 !== 0) return null;
|
|
9311
|
+
const names = [];
|
|
9312
|
+
for (let i = 0; i < parts.length; i += 2) {
|
|
9313
|
+
if (parts[i] !== "module" || !/^[A-Za-z0-9_-]+$/.test(parts[i + 1] ?? "")) return null;
|
|
9314
|
+
names.push(parts[i + 1]);
|
|
9315
|
+
}
|
|
9316
|
+
return names;
|
|
9317
|
+
}
|
|
9318
|
+
function configExpressions(plan, change) {
|
|
9319
|
+
return configResource(plan, change)?.expressions;
|
|
9320
|
+
}
|
|
9321
|
+
function configResource(plan, change) {
|
|
9322
|
+
if (!change.name) return void 0;
|
|
9323
|
+
const path3 = moduleNames(change.module_address);
|
|
9324
|
+
if (path3 === null) return void 0;
|
|
9325
|
+
let module = plan.configuration?.root_module;
|
|
9326
|
+
for (const name of path3) module = module?.module_calls?.[name]?.module;
|
|
9327
|
+
const address = `${change.type}.${change.name}`;
|
|
9328
|
+
return module?.resources?.find((r) => r.address === address && (r.mode ?? "managed") === "managed");
|
|
9329
|
+
}
|
|
9330
|
+
function knownAtPlan(expression, inRoot) {
|
|
9331
|
+
if ("constant_value" in expression) return true;
|
|
9332
|
+
const references = Array.isArray(expression.references) ? expression.references : [];
|
|
9333
|
+
return inRoot && references.length > 0 && references.every((r) => typeof r === "string" && r.startsWith("var."));
|
|
9334
|
+
}
|
|
9335
|
+
function hasUnknown(value) {
|
|
9336
|
+
if (value === true) return true;
|
|
9337
|
+
if (Array.isArray(value)) return value.some(hasUnknown);
|
|
9338
|
+
if (typeof value === "object" && value !== null) return Object.values(value).some(hasUnknown);
|
|
9339
|
+
return false;
|
|
9340
|
+
}
|
|
9341
|
+
var isPresent = (value) => Array.isArray(value) ? value.length > 0 : value !== null && value !== void 0 && value !== "";
|
|
9342
|
+
function unsetInConfig(change, expressions, names, blockedBy = [], blockedByBlocks = []) {
|
|
9343
|
+
const unknownSide = change.after_unknown;
|
|
9344
|
+
if (!names || !unknownSide || !expressions) return void 0;
|
|
9345
|
+
if (blockedBy.some((name) => name in expressions || isPresent(change.after?.[name]))) return void 0;
|
|
9346
|
+
if (blockedByBlocks.some((name) => name in expressions || isPresent(change.after?.[name]) || hasUnknown(unknownSide[name]))) return void 0;
|
|
9347
|
+
const unset = names.filter((name) => unknownSide[name] === true && !(name in expressions));
|
|
9348
|
+
return unset.length > 0 ? unset.join(",") : void 0;
|
|
9349
|
+
}
|
|
9350
|
+
function regionUntrusted(plan, change, regionAttribute) {
|
|
9351
|
+
if (change.change.after_unknown?.[regionAttribute] === true) return true;
|
|
9352
|
+
if (!isPresent(change.change.after?.[regionAttribute])) return false;
|
|
9353
|
+
const resource = configResource(plan, change);
|
|
9354
|
+
if (!resource?.expressions) return true;
|
|
9355
|
+
const expression = resource.expressions[regionAttribute];
|
|
9356
|
+
if (expression !== void 0) return !knownAtPlan(expression, (moduleNames(change.module_address) ?? ["?"]).length === 0);
|
|
9357
|
+
const key = resource.provider_config_key;
|
|
9358
|
+
const provider = key === void 0 ? void 0 : plan.configuration?.provider_config?.[key];
|
|
9359
|
+
if (!provider) return true;
|
|
9360
|
+
const providerRegion = provider.expressions?.["region"];
|
|
9361
|
+
return providerRegion !== void 0 && !knownAtPlan(providerRegion, provider.module_address === void 0);
|
|
9362
|
+
}
|
|
9272
9363
|
function pickAttributes(side, unknownSide, attributeNames) {
|
|
9273
9364
|
if (!side) return void 0;
|
|
9274
9365
|
const attrs = {};
|
|
@@ -9276,6 +9367,7 @@ function pickAttributes(side, unknownSide, attributeNames) {
|
|
|
9276
9367
|
if (unknownSide?.[name] === true) continue;
|
|
9277
9368
|
const value = side[name];
|
|
9278
9369
|
if (typeof value === "string" || typeof value === "number") attrs[name] = value;
|
|
9370
|
+
else if (typeof value === "boolean") attrs[name] = String(value);
|
|
9279
9371
|
}
|
|
9280
9372
|
return Object.keys(attrs).length > 0 ? attrs : void 0;
|
|
9281
9373
|
}
|
|
@@ -9288,7 +9380,17 @@ function parseTerraformPlan(plan) {
|
|
|
9288
9380
|
if (!action) continue;
|
|
9289
9381
|
const spec = IAC_RESOURCE_SPECS[resourceChange.type];
|
|
9290
9382
|
const attributesBefore = spec ? pickAttributes(resourceChange.change.before, void 0, spec.attributes) : void 0;
|
|
9291
|
-
const
|
|
9383
|
+
const picked = spec ? pickAttributes(resourceChange.change.after, resourceChange.change.after_unknown, spec.attributes) : void 0;
|
|
9384
|
+
const expressions = spec?.unsetReportable || spec?.regionPlannedByProvider ? configExpressions(plan, resourceChange) : void 0;
|
|
9385
|
+
const creates = action === "create" || action === "replace";
|
|
9386
|
+
const unset = creates ? unsetInConfig(resourceChange.change, expressions, spec?.unsetReportable, spec?.unsetBlockedBy, spec?.unsetBlockedByBlocks) : void 0;
|
|
9387
|
+
const regionAttribute = spec?.regionPlannedByProvider ? spec.regionAttribute : void 0;
|
|
9388
|
+
const untrusted = picked !== void 0 && regionAttribute !== void 0 && regionUntrusted(plan, resourceChange, regionAttribute);
|
|
9389
|
+
let attributesAfter = unset === void 0 ? picked : { ...picked, [IAC_UNSET_IN_CONFIG]: unset };
|
|
9390
|
+
if (untrusted && attributesAfter) {
|
|
9391
|
+
const { [regionAttribute]: _dropped, ...rest } = attributesAfter;
|
|
9392
|
+
attributesAfter = { ...rest, [IAC_UNKNOWN_IN_PLAN]: regionAttribute };
|
|
9393
|
+
}
|
|
9292
9394
|
const change = {
|
|
9293
9395
|
type: resourceChange.type,
|
|
9294
9396
|
action,
|
|
@@ -9319,6 +9421,9 @@ var REASON_TEXT = {
|
|
|
9319
9421
|
region_not_supported: "region not in the price catalog",
|
|
9320
9422
|
size_unknown: "size/SKU could not be resolved from the plan",
|
|
9321
9423
|
instance_count_unknown: "instance count could not be resolved from the plan",
|
|
9424
|
+
attribute_unknown: "a price-relevant attribute (engine, Multi-AZ, storage\u2026) could not be resolved from the plan",
|
|
9425
|
+
price_unavailable: "the price catalog did not answer this time; a later run retries",
|
|
9426
|
+
price_lookup_limit: "too many distinct database configurations in one plan to look up in a single request",
|
|
9322
9427
|
price_not_found: "no matching price found in the catalog"
|
|
9323
9428
|
};
|
|
9324
9429
|
var fmtUsd = (n) => `${n < 0 ? "-" : "+"}$${Math.abs(n).toFixed(2)}/mo`;
|
|
@@ -9326,7 +9431,7 @@ function fmtConverted(n, currency) {
|
|
|
9326
9431
|
const amount = new Intl.NumberFormat("en-US", { style: "currency", currency }).format(Math.abs(n));
|
|
9327
9432
|
return `\u2248 ${n < 0 ? "-" : "+"}${amount}/mo`;
|
|
9328
9433
|
}
|
|
9329
|
-
var LIST_PRICE_CAVEAT = "They also do not reflect committed-use discounts (EDP/EA/CUD) or existing reservations/Savings Plans that may already cover a resource.";
|
|
9434
|
+
var LIST_PRICE_CAVEAT = "They also do not reflect committed-use discounts (EDP/EA/CUD) or existing reservations/Savings Plans that may already cover a resource. Usage-based charges (data transfer, backups beyond the allocated storage, I/O requests, CPU credits), storage growth from autoscaling (`max_allocated_storage`), RDS Extended Support and paid Performance Insights retention are not included.";
|
|
9330
9435
|
var reasonText = (reason) => REASON_TEXT[reason] ?? reason;
|
|
9331
9436
|
function actionVerb(action) {
|
|
9332
9437
|
return action === "create" ? "new" : action === "delete" ? "removed" : action;
|
|
@@ -9357,8 +9462,16 @@ function renderComment(estimate) {
|
|
|
9357
9462
|
if (estimate.priced.length > 0) {
|
|
9358
9463
|
lines2.push("", "| Resource | Change | Qty | Delta |", "|---|---|---|---|");
|
|
9359
9464
|
for (const item of estimate.priced) {
|
|
9465
|
+
const assumed = item.assumedDefaults?.length ? " \u2020" : "";
|
|
9360
9466
|
lines2.push(
|
|
9361
|
-
`| \`${item.type}
|
|
9467
|
+
`| \`${item.type}\`${assumed} | ${actionVerb(item.action)} | ${item.count} | ${delta(item.monthlyDeltaUsd, item.monthlyDelta)} |`
|
|
9468
|
+
);
|
|
9469
|
+
}
|
|
9470
|
+
const assumedNames = [...new Set(estimate.priced.flatMap((i) => i.assumedDefaults ?? []))].sort();
|
|
9471
|
+
if (assumedNames.length > 0) {
|
|
9472
|
+
lines2.push(
|
|
9473
|
+
"",
|
|
9474
|
+
`\u2020 Not set in the Terraform configuration, so the AWS default was assumed for: ${assumedNames.map((n) => `\`${n}\``).join(", ")}. Set them explicitly if your deployment differs.`
|
|
9362
9475
|
);
|
|
9363
9476
|
}
|
|
9364
9477
|
}
|
|
@@ -9540,8 +9653,6 @@ function publicVulnerable(input, provider) {
|
|
|
9540
9653
|
}
|
|
9541
9654
|
|
|
9542
9655
|
// ../../services/security/src/rules/aws/combos.ts
|
|
9543
|
-
var DIA_MS = 24 * 60 * 60 * 1e3;
|
|
9544
|
-
var OLD_ACCESS_KEY_DAYS = 90;
|
|
9545
9656
|
function lambdaRef(resourceId, region) {
|
|
9546
9657
|
const partes = resourceId.split(":");
|
|
9547
9658
|
if (partes[0] === "arn" && partes[2] === "lambda" && partes[5] === "function" && partes[3] && partes[6]) {
|
|
@@ -9580,38 +9691,23 @@ var AWS_SECURITY_COMBOS = [
|
|
|
9580
9691
|
id: "COMBO_AWS_PUBLIC_DB_UNENCRYPTED",
|
|
9581
9692
|
provider: "aws",
|
|
9582
9693
|
severity: "critical",
|
|
9583
|
-
rules: ["SEC_AWS_ENCRYPTION_RDS_SIN_CIFRAR"],
|
|
9584
|
-
detect: (input) => input
|
|
9585
|
-
const db = observationOf(input, "rds_instance", f.region, f.resourceId);
|
|
9586
|
-
if (db?.attributes["publiclyAccessible"] !== true) return [];
|
|
9587
|
-
return [{ region: f.region, resourceId: f.resourceId, resourceName: f.resourceName, findingIds: [f.id], evidence: { publiclyAccessible: true } }];
|
|
9588
|
-
})
|
|
9694
|
+
rules: ["SEC_AWS_ENCRYPTION_RDS_SIN_CIFRAR", "SEC_AWS_EXPOSURE_RDS_PUBLICA"],
|
|
9695
|
+
detect: (input) => sameResource(input, "SEC_AWS_ENCRYPTION_RDS_SIN_CIFRAR", "SEC_AWS_EXPOSURE_RDS_PUBLICA").map((m) => ({ ...m, evidence: { publiclyAccessible: true } }))
|
|
9589
9696
|
},
|
|
9590
9697
|
{
|
|
9591
9698
|
// Entra a la consola solo con contraseña y además tiene una clave de acceso vieja y activa: dos puertas sin segundo factor.
|
|
9592
9699
|
id: "COMBO_AWS_USER_NO_MFA_OLD_KEY",
|
|
9593
9700
|
provider: "aws",
|
|
9594
9701
|
severity: "critical",
|
|
9595
|
-
rules: ["SEC_AWS_IAM_USUARIO_SIN_MFA"],
|
|
9596
|
-
detect: (input) =>
|
|
9597
|
-
const
|
|
9598
|
-
|
|
9599
|
-
|
|
9600
|
-
const
|
|
9601
|
-
return
|
|
9602
|
-
})
|
|
9603
|
-
|
|
9604
|
-
return [
|
|
9605
|
-
{
|
|
9606
|
-
region: f.region,
|
|
9607
|
-
resourceId: f.resourceId,
|
|
9608
|
-
resourceName: f.resourceName,
|
|
9609
|
-
findingIds: [f.id],
|
|
9610
|
-
// La fecha y no la edad: la edad la calcula quien la enseña, si no se quedaría congelada el día del escaneo.
|
|
9611
|
-
evidence: { oldActiveKeys: viejas.length, oldestKeyCreatedAt: viejas[0] }
|
|
9612
|
-
}
|
|
9613
|
-
];
|
|
9614
|
-
})
|
|
9702
|
+
rules: ["SEC_AWS_IAM_USUARIO_SIN_MFA", "SEC_AWS_IAM_CLAVE_ANTIGUA"],
|
|
9703
|
+
detect: (input) => {
|
|
9704
|
+
const viejas = findingsByResource(input.findings, "SEC_AWS_IAM_CLAVE_ANTIGUA");
|
|
9705
|
+
return sameResource(input, "SEC_AWS_IAM_USUARIO_SIN_MFA", "SEC_AWS_IAM_CLAVE_ANTIGUA").map((m) => {
|
|
9706
|
+
const claves = viejas.get(`${m.region}::${m.resourceId}`)?.evidence["oldActiveKeys"];
|
|
9707
|
+
const lista = Array.isArray(claves) ? claves : [];
|
|
9708
|
+
return { ...m, evidence: { oldActiveKeys: lista.length, oldestKeyCreatedAt: lista[0]?.["createdAt"] ?? null } };
|
|
9709
|
+
});
|
|
9710
|
+
}
|
|
9615
9711
|
},
|
|
9616
9712
|
{
|
|
9617
9713
|
// Alcanzable desde internet y con un CVE crítico o alto que ya tiene exploit público: el ataque está escrito.
|
|
@@ -10090,6 +10186,31 @@ var rdsClusterSnapshotPublico = {
|
|
|
10090
10186
|
],
|
|
10091
10187
|
evaluate: (observation) => snapshotPublicoEvaluate(observation, RDS_CHECKS.clusterSnapshotAttributes)
|
|
10092
10188
|
};
|
|
10189
|
+
function rdsPublicaEvaluate(observation) {
|
|
10190
|
+
const publica = observation.attributes.publiclyAccessible;
|
|
10191
|
+
if (typeof publica !== "boolean") {
|
|
10192
|
+
return { status: "unknown", check: RDS_CHECKS.instances, attribute: "publiclyAccessible" };
|
|
10193
|
+
}
|
|
10194
|
+
if (!publica) return { status: "ok" };
|
|
10195
|
+
return {
|
|
10196
|
+
status: "finding",
|
|
10197
|
+
evidence: {
|
|
10198
|
+
publiclyAccessible: true,
|
|
10199
|
+
engine: observation.attributes.engine ?? null,
|
|
10200
|
+
securityGroupIds: observation.attributes.securityGroupIds ?? null
|
|
10201
|
+
}
|
|
10202
|
+
};
|
|
10203
|
+
}
|
|
10204
|
+
var rdsPublica = {
|
|
10205
|
+
id: "SEC_AWS_EXPOSURE_RDS_PUBLICA",
|
|
10206
|
+
scope: "resource",
|
|
10207
|
+
category: "security_exposure",
|
|
10208
|
+
severity: "high",
|
|
10209
|
+
provider: "aws",
|
|
10210
|
+
kinds: ["rds_instance"],
|
|
10211
|
+
requires: [REGION_CHECKS.listRegions, RDS_CHECKS.instances],
|
|
10212
|
+
evaluate: rdsPublicaEvaluate
|
|
10213
|
+
};
|
|
10093
10214
|
|
|
10094
10215
|
// ../../services/security/src/rules/aws/iam.ts
|
|
10095
10216
|
function falta4(check, attribute) {
|
|
@@ -10143,6 +10264,74 @@ var usuarioSinMfa = {
|
|
|
10143
10264
|
requires: [IAM_CHECKS.listUsers, IAM_CHECKS.loginProfile, IAM_CHECKS.mfaDevices],
|
|
10144
10265
|
evaluate: usuarioSinMfaEvaluate
|
|
10145
10266
|
};
|
|
10267
|
+
var ACCESS_KEY_MAX_AGE_DAYS = 90;
|
|
10268
|
+
var DIA_MS = 864e5;
|
|
10269
|
+
function claveAntiguaEvaluate(observation, ahora = Date.now()) {
|
|
10270
|
+
const claves = observation.attributes.accessKeys;
|
|
10271
|
+
if (!Array.isArray(claves)) return falta4(IAM_CHECKS.accessKeys, "accessKeys");
|
|
10272
|
+
const viejas = [];
|
|
10273
|
+
let indecisa = false;
|
|
10274
|
+
for (const entrada of claves) {
|
|
10275
|
+
if (typeof entrada !== "object" || entrada === null) {
|
|
10276
|
+
indecisa = true;
|
|
10277
|
+
continue;
|
|
10278
|
+
}
|
|
10279
|
+
const clave = entrada;
|
|
10280
|
+
if (clave.active === false) continue;
|
|
10281
|
+
const creada = typeof clave.createdAt === "string" ? Date.parse(clave.createdAt) : Number.NaN;
|
|
10282
|
+
if (clave.active !== true || Number.isNaN(creada)) {
|
|
10283
|
+
indecisa = true;
|
|
10284
|
+
continue;
|
|
10285
|
+
}
|
|
10286
|
+
if (ahora - creada > ACCESS_KEY_MAX_AGE_DAYS * DIA_MS) viejas.push({ id: clave.id ?? null, createdAt: clave.createdAt });
|
|
10287
|
+
}
|
|
10288
|
+
if (viejas.length === 0) return indecisa ? falta4(IAM_CHECKS.accessKeys, "accessKeys") : { status: "ok" };
|
|
10289
|
+
viejas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
10290
|
+
return { status: "finding", evidence: { oldActiveKeys: viejas, maxAgeDays: ACCESS_KEY_MAX_AGE_DAYS } };
|
|
10291
|
+
}
|
|
10292
|
+
var claveAntigua = {
|
|
10293
|
+
id: "SEC_AWS_IAM_CLAVE_ANTIGUA",
|
|
10294
|
+
scope: "resource",
|
|
10295
|
+
category: "security_iam",
|
|
10296
|
+
severity: "medium",
|
|
10297
|
+
provider: "aws",
|
|
10298
|
+
kinds: ["iam_user"],
|
|
10299
|
+
requires: [IAM_CHECKS.listUsers, IAM_CHECKS.accessKeys],
|
|
10300
|
+
evaluate: (observation) => claveAntiguaEvaluate(observation)
|
|
10301
|
+
};
|
|
10302
|
+
var PASSWORD_MIN_LENGTH = 14;
|
|
10303
|
+
var PASSWORD_REUSE_PREVENTION = 24;
|
|
10304
|
+
function politicaContrasenasEvaluate(observation) {
|
|
10305
|
+
const usuarios = observation.attributes.userCount;
|
|
10306
|
+
if (typeof usuarios !== "number") return falta4(IAM_CHECKS.accountSummary, "userCount");
|
|
10307
|
+
if (usuarios === 0) return { status: "ok" };
|
|
10308
|
+
if (!("passwordPolicy" in observation.attributes)) return falta4(IAM_CHECKS.passwordPolicy, "passwordPolicy");
|
|
10309
|
+
const politica = observation.attributes.passwordPolicy;
|
|
10310
|
+
if (politica === null) {
|
|
10311
|
+
return { status: "finding", evidence: { passwordPolicy: null, userCount: usuarios } };
|
|
10312
|
+
}
|
|
10313
|
+
if (typeof politica !== "object" || Array.isArray(politica)) return falta4(IAM_CHECKS.passwordPolicy, "passwordPolicy");
|
|
10314
|
+
const p = politica;
|
|
10315
|
+
const longitud = typeof p.minimumLength === "number" ? p.minimumLength : null;
|
|
10316
|
+
const reuso = typeof p.reusePrevention === "number" ? p.reusePrevention : null;
|
|
10317
|
+
const corta = longitud === null || longitud < PASSWORD_MIN_LENGTH;
|
|
10318
|
+
const reutilizable = reuso === null || reuso < PASSWORD_REUSE_PREVENTION;
|
|
10319
|
+
if (!corta && !reutilizable) return { status: "ok" };
|
|
10320
|
+
return {
|
|
10321
|
+
status: "finding",
|
|
10322
|
+
evidence: { minimumLength: longitud, reusePrevention: reuso, userCount: usuarios }
|
|
10323
|
+
};
|
|
10324
|
+
}
|
|
10325
|
+
var politicaContrasenas = {
|
|
10326
|
+
id: "SEC_AWS_IAM_POLITICA_CONTRASENAS",
|
|
10327
|
+
scope: "resource",
|
|
10328
|
+
category: "security_iam",
|
|
10329
|
+
severity: "low",
|
|
10330
|
+
provider: "aws",
|
|
10331
|
+
kinds: ["iam_account"],
|
|
10332
|
+
requires: [IAM_CHECKS.accountSummary, IAM_CHECKS.passwordPolicy],
|
|
10333
|
+
evaluate: politicaContrasenasEvaluate
|
|
10334
|
+
};
|
|
10146
10335
|
function exclusion(observation) {
|
|
10147
10336
|
const value = observation.attributes.excluded;
|
|
10148
10337
|
if (value === null) return { known: true, excluded: false };
|
|
@@ -10559,8 +10748,9 @@ var trailGestionIncompleta = {
|
|
|
10559
10748
|
evaluate: gestionIncompletaEvaluate
|
|
10560
10749
|
};
|
|
10561
10750
|
|
|
10562
|
-
// ../../
|
|
10751
|
+
// ../../shared/src/internet-exposure.ts
|
|
10563
10752
|
var ADMIN_PORTS2 = [22, 3389];
|
|
10753
|
+
var DATABASE_PORTS = [1433, 1521, 2483, 2484, 3306, 5432, 6379, 9200, 9300, 27017, 27018, 27019];
|
|
10564
10754
|
var ETIQUETAS_INTERNET = /* @__PURE__ */ new Set(["*", "internet", "any", "/0"]);
|
|
10565
10755
|
function longitudPrefijo(cidr, maximo) {
|
|
10566
10756
|
const partes = cidr.split("/");
|
|
@@ -10601,13 +10791,57 @@ function protocolName(protocol) {
|
|
|
10601
10791
|
const p = protocol.trim().toLowerCase();
|
|
10602
10792
|
return POR_NUMERO2[p] ?? p;
|
|
10603
10793
|
}
|
|
10604
|
-
function
|
|
10794
|
+
function portsReached(ports, protocol, portRanges) {
|
|
10605
10795
|
const p = protocolName(protocol);
|
|
10606
10796
|
const todos = p === "*" || p === "any" || p === "all";
|
|
10607
|
-
if (todos) return [...
|
|
10797
|
+
if (todos) return [...ports];
|
|
10608
10798
|
if (p !== "tcp" && p !== "udp") return [];
|
|
10609
|
-
if (portRanges.length === 0) return [...
|
|
10610
|
-
return
|
|
10799
|
+
if (portRanges.length === 0) return [...ports];
|
|
10800
|
+
return ports.filter((puerto) => portRanges.some((rango) => cubrePuerto(rango, puerto)));
|
|
10801
|
+
}
|
|
10802
|
+
function adminPortsReached(protocol, portRanges) {
|
|
10803
|
+
return portsReached(ADMIN_PORTS2, protocol, portRanges);
|
|
10804
|
+
}
|
|
10805
|
+
function databasePortsReached(protocol, portRanges) {
|
|
10806
|
+
if (protocolName(protocol) === "udp") return [];
|
|
10807
|
+
if (adminPortsReached(protocol, portRanges).length > 0) return [];
|
|
10808
|
+
return portsReached(DATABASE_PORTS, protocol, portRanges);
|
|
10809
|
+
}
|
|
10810
|
+
var TODO_INTERNET = /* @__PURE__ */ new Set(["*", "internet", "any"]);
|
|
10811
|
+
function sourceCoversInternet(source) {
|
|
10812
|
+
return TODO_INTERNET.has(source.trim().toLowerCase());
|
|
10813
|
+
}
|
|
10814
|
+
function ipFamily(source) {
|
|
10815
|
+
const ip = source.trim().split("/")[0] ?? "";
|
|
10816
|
+
if (ip.includes(":")) return "v6";
|
|
10817
|
+
return /^\d{1,3}(\.\d{1,3}){3}$/.test(ip) ? "v4" : void 0;
|
|
10818
|
+
}
|
|
10819
|
+
function denySourceCovers(denySource, allowSource) {
|
|
10820
|
+
if (sourceCoversInternet(denySource)) return true;
|
|
10821
|
+
const partes = denySource.trim().split("/");
|
|
10822
|
+
if (partes.length !== 2 || partes[1] !== "0") return false;
|
|
10823
|
+
const familia = ipFamily(denySource);
|
|
10824
|
+
return familia !== void 0 && familia === ipFamily(allowSource);
|
|
10825
|
+
}
|
|
10826
|
+
function denyCoversEveryTarget(deny) {
|
|
10827
|
+
if ((deny.destinationAsgs ?? 0) > 0) return false;
|
|
10828
|
+
if ((deny.destinations ?? []).some((d) => !["*", "any"].includes(d.trim().toLowerCase()))) return false;
|
|
10829
|
+
return (deny.sourcePorts ?? []).every((p) => p.trim() === "*");
|
|
10830
|
+
}
|
|
10831
|
+
function deniedBeforeAllow(denies, allow, port) {
|
|
10832
|
+
const allowPriority = allow.priority;
|
|
10833
|
+
if (typeof allowPriority !== "number") return false;
|
|
10834
|
+
const abiertos = allow.sources.filter((s) => sourceOpensToInternet(s) === true);
|
|
10835
|
+
if (abiertos.length === 0) return false;
|
|
10836
|
+
const allowProtocol = protocolName(allow.protocol);
|
|
10837
|
+
return denies.some((deny) => {
|
|
10838
|
+
if (typeof deny.priority !== "number" || deny.priority >= allowPriority) return false;
|
|
10839
|
+
if (!denyCoversEveryTarget(deny)) return false;
|
|
10840
|
+
if (!abiertos.every((a) => deny.sources.some((d) => denySourceCovers(d, a)))) return false;
|
|
10841
|
+
const p = protocolName(deny.protocol);
|
|
10842
|
+
if (p !== "*" && p !== "any" && p !== "all" && p !== allowProtocol) return false;
|
|
10843
|
+
return deny.ports.length === 0 || deny.ports.some((rango) => cubrePuerto(rango, port));
|
|
10844
|
+
});
|
|
10611
10845
|
}
|
|
10612
10846
|
function cadenas2(value) {
|
|
10613
10847
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
@@ -10843,6 +11077,67 @@ var adminAbiertoPorPrefixList = {
|
|
|
10843
11077
|
requires: [REGION_CHECKS.listRegions, EC2_CHECKS.securityGroups, EC2_CHECKS.prefixListEntries],
|
|
10844
11078
|
evaluate: prefixListEvaluate
|
|
10845
11079
|
};
|
|
11080
|
+
function puertosDeBaseDeDatos(regla) {
|
|
11081
|
+
if (typeof regla.protocol !== "string") return void 0;
|
|
11082
|
+
const protocolo = protocolName(regla.protocol);
|
|
11083
|
+
if (protocolo !== "tcp") return [];
|
|
11084
|
+
const desde = regla.fromPort;
|
|
11085
|
+
const hasta = regla.toPort;
|
|
11086
|
+
if (typeof desde !== "number" || typeof hasta !== "number") return void 0;
|
|
11087
|
+
if (ADMIN_PORTS2.some((puerto) => desde <= puerto && puerto <= hasta)) return [];
|
|
11088
|
+
return DATABASE_PORTS.filter((puerto) => desde <= puerto && puerto <= hasta);
|
|
11089
|
+
}
|
|
11090
|
+
function bdAbiertaEvaluate(observation) {
|
|
11091
|
+
const ingress = observation.attributes.ingressRules;
|
|
11092
|
+
if (!Array.isArray(ingress)) {
|
|
11093
|
+
return { status: "unknown", check: EC2_CHECKS.securityGroups, attribute: "ingressRules" };
|
|
11094
|
+
}
|
|
11095
|
+
const aperturas = [];
|
|
11096
|
+
const puertos = /* @__PURE__ */ new Set();
|
|
11097
|
+
let indecisa = false;
|
|
11098
|
+
for (const entrada of ingress) {
|
|
11099
|
+
if (typeof entrada !== "object" || entrada === null) {
|
|
11100
|
+
indecisa = true;
|
|
11101
|
+
continue;
|
|
11102
|
+
}
|
|
11103
|
+
const regla = entrada;
|
|
11104
|
+
const origenes = [...cadenas2(regla.cidrs), ...cadenas2(regla.ipv6Cidrs)];
|
|
11105
|
+
const abierto = anySourceOpensToInternet(origenes);
|
|
11106
|
+
if (abierto === void 0) {
|
|
11107
|
+
indecisa = true;
|
|
11108
|
+
continue;
|
|
11109
|
+
}
|
|
11110
|
+
if (!abierto) continue;
|
|
11111
|
+
const alcanzados = puertosDeBaseDeDatos(regla);
|
|
11112
|
+
if (alcanzados === void 0) {
|
|
11113
|
+
indecisa = true;
|
|
11114
|
+
continue;
|
|
11115
|
+
}
|
|
11116
|
+
if (alcanzados.length === 0) continue;
|
|
11117
|
+
for (const puerto of alcanzados) puertos.add(puerto);
|
|
11118
|
+
aperturas.push({
|
|
11119
|
+
protocol: regla.protocol,
|
|
11120
|
+
fromPort: regla.fromPort ?? null,
|
|
11121
|
+
toPort: regla.toPort ?? null,
|
|
11122
|
+
cidr: origenes.find((cidr) => sourceOpensToInternet(cidr) === true) ?? null
|
|
11123
|
+
});
|
|
11124
|
+
}
|
|
11125
|
+
if (aperturas.length > 0) {
|
|
11126
|
+
return { status: "finding", evidence: { ports: [...puertos].sort((a, b) => a - b), openings: aperturas } };
|
|
11127
|
+
}
|
|
11128
|
+
if (indecisa) return { status: "unknown", check: EC2_CHECKS.securityGroups, attribute: "ingressRules" };
|
|
11129
|
+
return { status: "ok" };
|
|
11130
|
+
}
|
|
11131
|
+
var bdAbiertaAInternet = {
|
|
11132
|
+
id: "SEC_AWS_NETWORK_BD_ABIERTO",
|
|
11133
|
+
scope: "resource",
|
|
11134
|
+
category: "security_network",
|
|
11135
|
+
severity: "high",
|
|
11136
|
+
provider: "aws",
|
|
11137
|
+
kinds: ["security_group"],
|
|
11138
|
+
requires: [REGION_CHECKS.listRegions, EC2_CHECKS.securityGroups],
|
|
11139
|
+
evaluate: bdAbiertaEvaluate
|
|
11140
|
+
};
|
|
10846
11141
|
|
|
10847
11142
|
// ../../services/security/src/rules/aws/monitoring.ts
|
|
10848
11143
|
var evento = (...nombres) => nombres.map((n) => `$.eventName=${n}`);
|
|
@@ -11174,17 +11469,21 @@ var secretoEnVariableDeEntorno = {
|
|
|
11174
11469
|
var AWS_SECURITY_RULES = [
|
|
11175
11470
|
adminAbiertoAInternet,
|
|
11176
11471
|
adminAbiertoPorPrefixList,
|
|
11472
|
+
bdAbiertaAInternet,
|
|
11177
11473
|
lightsailAdminAbierto,
|
|
11178
11474
|
nlbAdminAbierto,
|
|
11179
11475
|
bucketPublico,
|
|
11180
11476
|
ebsSnapshotPublico,
|
|
11181
11477
|
rdsSnapshotPublico,
|
|
11182
11478
|
rdsClusterSnapshotPublico,
|
|
11479
|
+
rdsPublica,
|
|
11183
11480
|
lambdaPublica,
|
|
11184
11481
|
kmsSinRotacion,
|
|
11185
11482
|
rdsSinCifrar,
|
|
11186
11483
|
rootSinMfa,
|
|
11187
11484
|
usuarioSinMfa,
|
|
11485
|
+
claveAntigua,
|
|
11486
|
+
politicaContrasenas,
|
|
11188
11487
|
adminEfectivo,
|
|
11189
11488
|
identidadSinUso,
|
|
11190
11489
|
claveSinUso,
|
|
@@ -11233,17 +11532,35 @@ var AZURE_NSG_CHECKS = {
|
|
|
11233
11532
|
list: "azure:nsg:list"
|
|
11234
11533
|
};
|
|
11235
11534
|
var AZURE_STORAGE_CHECKS = {
|
|
11236
|
-
list: "azure:storage:list"
|
|
11535
|
+
list: "azure:storage:list",
|
|
11536
|
+
// Solo de las cuentas que permiten blobs públicos: dice a quien aprueba cerrarlo qué contenedores dejan de servirse.
|
|
11537
|
+
containers: "azure:storage:containers"
|
|
11237
11538
|
};
|
|
11238
11539
|
var AZURE_SQL_CHECKS = {
|
|
11239
11540
|
list: "azure:sql:list",
|
|
11240
|
-
firewallRules: "azure:sql:firewallRules"
|
|
11541
|
+
firewallRules: "azure:sql:firewallRules",
|
|
11542
|
+
databases: "azure:sql:databases",
|
|
11543
|
+
transparentDataEncryption: "azure:sql:transparentDataEncryption"
|
|
11544
|
+
};
|
|
11545
|
+
var AZURE_KEYVAULT_CHECKS = {
|
|
11546
|
+
vaults: "azure:keyvault:vaults"
|
|
11547
|
+
};
|
|
11548
|
+
var AZURE_NETWORK_CHECKS = {
|
|
11549
|
+
virtualNetworks: "azure:network:virtualNetworks",
|
|
11550
|
+
networkWatchers: "azure:network:networkWatchers",
|
|
11551
|
+
flowLogs: "azure:network:flowLogs",
|
|
11552
|
+
networkInterfaces: "azure:network:networkInterfaces"
|
|
11241
11553
|
};
|
|
11242
11554
|
var AZURE_IAM_CHECKS = {
|
|
11243
11555
|
roleAssignments: "azure:iam:roleAssignments",
|
|
11244
11556
|
// Microsoft Graph, opcional: `Directory.Read.All` de aplicación (§16.7, F4).
|
|
11245
11557
|
principals: "azure:graph:directoryObjects.getByIds",
|
|
11246
|
-
groupMembers: "azure:graph:groups.members"
|
|
11558
|
+
groupMembers: "azure:graph:groups.members",
|
|
11559
|
+
// Tipo, estado y dominio de los usuarios con rol privilegiado, y los dominios federados: también con `Directory.Read.All`.
|
|
11560
|
+
users: "azure:graph:users",
|
|
11561
|
+
domains: "azure:graph:domains",
|
|
11562
|
+
// Métodos de autenticación registrados: `UserAuthenticationMethod.Read.All` de aplicación, opcional.
|
|
11563
|
+
authMethods: "azure:graph:users.authentication.methods"
|
|
11247
11564
|
};
|
|
11248
11565
|
var AZURE_LOGGING_CHECKS = {
|
|
11249
11566
|
diagnosticSettings: "azure:insights:diagnosticSettings"
|
|
@@ -11296,6 +11613,44 @@ var sqlTlsDebil = {
|
|
|
11296
11613
|
requires: [AZURE_SQL_CHECKS.list],
|
|
11297
11614
|
evaluate: sqlTlsEvaluate
|
|
11298
11615
|
};
|
|
11616
|
+
function keyVaultSinPurgaEvaluate(observation) {
|
|
11617
|
+
if (!("purgeProtection" in observation.attributes)) {
|
|
11618
|
+
return { status: "unknown", check: AZURE_KEYVAULT_CHECKS.vaults, attribute: "purgeProtection" };
|
|
11619
|
+
}
|
|
11620
|
+
if (observation.attributes.purgeProtection === true) return { status: "ok" };
|
|
11621
|
+
return {
|
|
11622
|
+
status: "finding",
|
|
11623
|
+
evidence: { purgeProtection: false, softDelete: observation.attributes.softDelete ?? null }
|
|
11624
|
+
};
|
|
11625
|
+
}
|
|
11626
|
+
var keyVaultSinPurga = {
|
|
11627
|
+
id: "SEC_AZURE_ENCRYPTION_KEYVAULT_SIN_PURGA",
|
|
11628
|
+
scope: "resource",
|
|
11629
|
+
category: "security_encryption",
|
|
11630
|
+
severity: "medium",
|
|
11631
|
+
provider: "azure",
|
|
11632
|
+
kinds: ["key_vault"],
|
|
11633
|
+
requires: [AZURE_KEYVAULT_CHECKS.vaults],
|
|
11634
|
+
evaluate: keyVaultSinPurgaEvaluate
|
|
11635
|
+
};
|
|
11636
|
+
function sqlSinTdeEvaluate(observation) {
|
|
11637
|
+
const estado = observation.attributes.tdeState;
|
|
11638
|
+
if (typeof estado !== "string") {
|
|
11639
|
+
return { status: "unknown", check: AZURE_SQL_CHECKS.transparentDataEncryption, attribute: "tdeState" };
|
|
11640
|
+
}
|
|
11641
|
+
if (estado.toLowerCase() === "enabled") return { status: "ok" };
|
|
11642
|
+
return { status: "finding", evidence: { tdeState: estado } };
|
|
11643
|
+
}
|
|
11644
|
+
var sqlSinTde = {
|
|
11645
|
+
id: "SEC_AZURE_ENCRYPTION_SQL_SIN_TDE",
|
|
11646
|
+
scope: "resource",
|
|
11647
|
+
category: "security_encryption",
|
|
11648
|
+
severity: "high",
|
|
11649
|
+
provider: "azure",
|
|
11650
|
+
kinds: ["sql_database"],
|
|
11651
|
+
requires: [AZURE_SQL_CHECKS.list, AZURE_SQL_CHECKS.databases, AZURE_SQL_CHECKS.transparentDataEncryption],
|
|
11652
|
+
evaluate: sqlSinTdeEvaluate
|
|
11653
|
+
};
|
|
11299
11654
|
|
|
11300
11655
|
// ../../services/security/src/rules/azure/exposure.ts
|
|
11301
11656
|
function blobPublicoEvaluate(observation) {
|
|
@@ -11304,7 +11659,15 @@ function blobPublicoEvaluate(observation) {
|
|
|
11304
11659
|
return { status: "unknown", check: AZURE_STORAGE_CHECKS.list, attribute: "allowBlobPublicAccess" };
|
|
11305
11660
|
}
|
|
11306
11661
|
if (!permitido) return { status: "ok" };
|
|
11307
|
-
|
|
11662
|
+
const contenedores = observation.attributes.publicContainers;
|
|
11663
|
+
const lista = Array.isArray(contenedores) ? contenedores.filter((c) => typeof c === "string") : void 0;
|
|
11664
|
+
return {
|
|
11665
|
+
status: "finding",
|
|
11666
|
+
evidence: {
|
|
11667
|
+
allowBlobPublicAccess: true,
|
|
11668
|
+
...lista ? { publicContainers: lista, publicContainersTruncated: observation.attributes.publicContainersTruncated === true } : {}
|
|
11669
|
+
}
|
|
11670
|
+
};
|
|
11308
11671
|
}
|
|
11309
11672
|
var storageBlobPublico = {
|
|
11310
11673
|
id: "SEC_AZURE_EXPOSURE_BLOB_PUBLICO",
|
|
@@ -11377,6 +11740,44 @@ var rolPrivilegiado = {
|
|
|
11377
11740
|
requires: [AZURE_IAM_CHECKS.roleAssignments],
|
|
11378
11741
|
evaluate: evaluate4
|
|
11379
11742
|
};
|
|
11743
|
+
function sinMfaEvaluate(observation) {
|
|
11744
|
+
const role = observation.attributes.role;
|
|
11745
|
+
const atSubscription = observation.attributes.subscriptionScope;
|
|
11746
|
+
if (typeof role !== "string") return { status: "unknown", check: AZURE_IAM_CHECKS.roleAssignments, attribute: "role" };
|
|
11747
|
+
if (typeof atSubscription !== "boolean") return { status: "unknown", check: AZURE_IAM_CHECKS.roleAssignments, attribute: "subscriptionScope" };
|
|
11748
|
+
const privilegiado = role === "Owner" || role === "UserAccessAdministrator" || role === "Contributor" && atSubscription;
|
|
11749
|
+
if (!privilegiado || observation.attributes.principalType !== "User") return { status: "ok" };
|
|
11750
|
+
if (observation.attributes.principalExists === false) return { status: "ok" };
|
|
11751
|
+
if (observation.attributes.mfaSampled === false) {
|
|
11752
|
+
return { status: "unknown", check: AZURE_IAM_CHECKS.users, attribute: "mfaScope", reason: "sample_capped" };
|
|
11753
|
+
}
|
|
11754
|
+
const alcance = observation.attributes.mfaScope;
|
|
11755
|
+
if (typeof alcance !== "string") return { status: "unknown", check: AZURE_IAM_CHECKS.users, attribute: "mfaScope" };
|
|
11756
|
+
if (alcance !== "checked") return { status: "ok" };
|
|
11757
|
+
const segundo = observation.attributes.secondFactor;
|
|
11758
|
+
if (typeof segundo !== "boolean") return { status: "unknown", check: AZURE_IAM_CHECKS.authMethods, attribute: "secondFactor" };
|
|
11759
|
+
if (segundo) return { status: "ok" };
|
|
11760
|
+
return {
|
|
11761
|
+
status: "finding",
|
|
11762
|
+
evidence: {
|
|
11763
|
+
role,
|
|
11764
|
+
scope: observation.attributes.scope ?? null,
|
|
11765
|
+
authMethods: observation.attributes.authMethods ?? [],
|
|
11766
|
+
...pick2(observation, ["principalName"])
|
|
11767
|
+
}
|
|
11768
|
+
};
|
|
11769
|
+
}
|
|
11770
|
+
var adminSinMfa = {
|
|
11771
|
+
id: "SEC_AZURE_IAM_ADMIN_SIN_MFA",
|
|
11772
|
+
scope: "resource",
|
|
11773
|
+
category: "security_iam",
|
|
11774
|
+
severity: "high",
|
|
11775
|
+
provider: "azure",
|
|
11776
|
+
kinds: ["role_assignment"],
|
|
11777
|
+
requires: [AZURE_IAM_CHECKS.roleAssignments, AZURE_IAM_CHECKS.domains, AZURE_IAM_CHECKS.users, AZURE_IAM_CHECKS.authMethods],
|
|
11778
|
+
runtimeOnly: true,
|
|
11779
|
+
evaluate: sinMfaEvaluate
|
|
11780
|
+
};
|
|
11380
11781
|
function huerfanaEvaluate(observation) {
|
|
11381
11782
|
if (observation.attributes.orphanApplicable === false) return { status: "ok" };
|
|
11382
11783
|
const exists = observation.attributes.principalExists;
|
|
@@ -11450,9 +11851,52 @@ var sinDiagnosticSettings = {
|
|
|
11450
11851
|
requires: [AZURE_LOGGING_CHECKS.diagnosticSettings],
|
|
11451
11852
|
evaluate: evaluate5
|
|
11452
11853
|
};
|
|
11854
|
+
function vnetSinFlowLogsEvaluate(observation) {
|
|
11855
|
+
const vnet = observation.attributes.vnetFlowLog;
|
|
11856
|
+
if (typeof vnet !== "boolean") {
|
|
11857
|
+
return { status: "unknown", check: AZURE_NETWORK_CHECKS.flowLogs, attribute: "vnetFlowLog" };
|
|
11858
|
+
}
|
|
11859
|
+
if (vnet) return { status: "ok" };
|
|
11860
|
+
const subnets = observation.attributes.subnets;
|
|
11861
|
+
if (!Array.isArray(subnets)) {
|
|
11862
|
+
return { status: "unknown", check: AZURE_NETWORK_CHECKS.networkInterfaces, attribute: "subnets" };
|
|
11863
|
+
}
|
|
11864
|
+
const sinRegistro = subnets.filter((subnet) => subnet.covered !== true && subnet.platform !== true).map((subnet) => subnet.name);
|
|
11865
|
+
if (sinRegistro.length === 0) return { status: "ok" };
|
|
11866
|
+
return { status: "finding", evidence: { unloggedSubnets: sinRegistro, subnetCount: subnets.length } };
|
|
11867
|
+
}
|
|
11868
|
+
var vnetSinFlowLogs = {
|
|
11869
|
+
id: "SEC_AZURE_LOGGING_VNET_SIN_FLOW_LOGS",
|
|
11870
|
+
scope: "resource",
|
|
11871
|
+
category: "security_logging",
|
|
11872
|
+
severity: "medium",
|
|
11873
|
+
provider: "azure",
|
|
11874
|
+
kinds: ["virtual_network"],
|
|
11875
|
+
// Las interfaces no van aquí: solo cuentan para subredes sin log propio, y su falta ya sale como `unknown` de esa VNet.
|
|
11876
|
+
requires: [AZURE_NETWORK_CHECKS.virtualNetworks, AZURE_NETWORK_CHECKS.networkWatchers, AZURE_NETWORK_CHECKS.flowLogs],
|
|
11877
|
+
evaluate: vnetSinFlowLogsEvaluate
|
|
11878
|
+
};
|
|
11453
11879
|
|
|
11454
11880
|
// ../../services/security/src/rules/azure/network.ts
|
|
11455
|
-
function
|
|
11881
|
+
function denyEntries(value) {
|
|
11882
|
+
if (!Array.isArray(value)) return [];
|
|
11883
|
+
return value.flatMap((entrada) => {
|
|
11884
|
+
if (typeof entrada !== "object" || entrada === null) return [];
|
|
11885
|
+
const regla = entrada;
|
|
11886
|
+
if (typeof regla.priority !== "number" || !Array.isArray(regla.destinations) || !Array.isArray(regla.sourcePorts)) return [];
|
|
11887
|
+
return [{
|
|
11888
|
+
protocol: typeof regla.protocol === "string" ? regla.protocol : "",
|
|
11889
|
+
sources: cadenas2(regla.sources),
|
|
11890
|
+
ports: cadenas2(regla.ports),
|
|
11891
|
+
priority: regla.priority,
|
|
11892
|
+
destinations: cadenas2(regla.destinations),
|
|
11893
|
+
destinationAsgs: typeof regla.destinationAsgs === "number" ? regla.destinationAsgs : 0,
|
|
11894
|
+
sourcePorts: cadenas2(regla.sourcePorts)
|
|
11895
|
+
}];
|
|
11896
|
+
});
|
|
11897
|
+
}
|
|
11898
|
+
var evaluate6 = (observation) => nsgAbierto(observation, adminPortsReached);
|
|
11899
|
+
function nsgAbierto(observation, reached) {
|
|
11456
11900
|
const ingress = observation.attributes.ingressRules;
|
|
11457
11901
|
if (!Array.isArray(ingress)) {
|
|
11458
11902
|
return { status: "unknown", check: AZURE_NSG_CHECKS.list, attribute: "ingressRules" };
|
|
@@ -11460,6 +11904,7 @@ function evaluate6(observation) {
|
|
|
11460
11904
|
const openings = [];
|
|
11461
11905
|
const puertos = /* @__PURE__ */ new Set();
|
|
11462
11906
|
let indecisa = false;
|
|
11907
|
+
const denegaciones = denyEntries(observation.attributes.denyIngressRules);
|
|
11463
11908
|
for (const entrada of ingress) {
|
|
11464
11909
|
if (typeof entrada !== "object" || entrada === null) {
|
|
11465
11910
|
indecisa = true;
|
|
@@ -11474,10 +11919,16 @@ function evaluate6(observation) {
|
|
|
11474
11919
|
}
|
|
11475
11920
|
if (!abierto) continue;
|
|
11476
11921
|
const protocol = typeof regla.protocol === "string" ? regla.protocol : "";
|
|
11477
|
-
const
|
|
11922
|
+
const priority = typeof regla.priority === "number" ? regla.priority : void 0;
|
|
11923
|
+
const alcanzados = reached(protocol, cadenas2(regla.ports)).filter((puerto) => !deniedBeforeAllow(denegaciones, { protocol, priority, sources }, puerto));
|
|
11478
11924
|
if (alcanzados.length === 0) continue;
|
|
11479
11925
|
for (const puerto of alcanzados) puertos.add(puerto);
|
|
11480
|
-
openings.push({
|
|
11926
|
+
openings.push({
|
|
11927
|
+
protocol: regla.protocol,
|
|
11928
|
+
ports: alcanzados,
|
|
11929
|
+
sources: regla.sources,
|
|
11930
|
+
...typeof regla.name === "string" ? { ruleName: regla.name } : {}
|
|
11931
|
+
});
|
|
11481
11932
|
}
|
|
11482
11933
|
if (openings.length > 0) {
|
|
11483
11934
|
return { status: "finding", evidence: { ports: [...puertos].sort((a, b) => a - b), openings } };
|
|
@@ -11497,17 +11948,32 @@ var nsgAdminAbierto = {
|
|
|
11497
11948
|
requires: [AZURE_NSG_CHECKS.list],
|
|
11498
11949
|
evaluate: evaluate6
|
|
11499
11950
|
};
|
|
11951
|
+
var nsgBdAbierto = {
|
|
11952
|
+
id: "SEC_AZURE_NETWORK_BD_ABIERTO",
|
|
11953
|
+
scope: "resource",
|
|
11954
|
+
category: "security_network",
|
|
11955
|
+
severity: "high",
|
|
11956
|
+
provider: "azure",
|
|
11957
|
+
kinds: ["nsg"],
|
|
11958
|
+
requires: [AZURE_NSG_CHECKS.list],
|
|
11959
|
+
evaluate: (observation) => nsgAbierto(observation, databasePortsReached)
|
|
11960
|
+
};
|
|
11500
11961
|
|
|
11501
11962
|
// ../../services/security/src/rules/azure/index.ts
|
|
11502
11963
|
var AZURE_SECURITY_RULES = [
|
|
11503
11964
|
nsgAdminAbierto,
|
|
11965
|
+
nsgBdAbierto,
|
|
11504
11966
|
storageBlobPublico,
|
|
11505
11967
|
sqlPublico,
|
|
11506
11968
|
storageTlsDebil,
|
|
11507
11969
|
sqlTlsDebil,
|
|
11970
|
+
keyVaultSinPurga,
|
|
11971
|
+
sqlSinTde,
|
|
11508
11972
|
rolPrivilegiado,
|
|
11509
11973
|
asignacionHuerfana,
|
|
11510
|
-
|
|
11974
|
+
adminSinMfa,
|
|
11975
|
+
sinDiagnosticSettings,
|
|
11976
|
+
vnetSinFlowLogs
|
|
11511
11977
|
];
|
|
11512
11978
|
|
|
11513
11979
|
// ../../services/security/src/rules/gcp/combos.ts
|
|
@@ -11579,7 +12045,16 @@ var GCP_COMPUTE_CHECKS = {
|
|
|
11579
12045
|
effectiveFirewalls: "gcp:compute:networks.getEffectiveFirewalls",
|
|
11580
12046
|
regions: "gcp:compute:regions",
|
|
11581
12047
|
regionFirewallPolicies: "gcp:compute:regionNetworkFirewallPolicies",
|
|
11582
|
-
sharedVpcHost: "gcp:compute:sharedVpcHostFirewalls"
|
|
12048
|
+
sharedVpcHost: "gcp:compute:sharedVpcHostFirewalls",
|
|
12049
|
+
subnetworks: "gcp:compute:subnetworks"
|
|
12050
|
+
};
|
|
12051
|
+
var GCP_NETWORK_MANAGEMENT_CHECKS = {
|
|
12052
|
+
vpcFlowLogsConfigs: "gcp:networkmanagement:vpcFlowLogsConfigs"
|
|
12053
|
+
};
|
|
12054
|
+
var GCP_KMS_CHECKS = {
|
|
12055
|
+
locations: "gcp:kms:locations",
|
|
12056
|
+
keyRings: "gcp:kms:keyRings",
|
|
12057
|
+
cryptoKeys: "gcp:kms:cryptoKeys"
|
|
11583
12058
|
};
|
|
11584
12059
|
var GCP_SQL_CHECKS = {
|
|
11585
12060
|
instances: "gcp:sql:instances"
|
|
@@ -11649,6 +12124,63 @@ var sqlSinCmek = {
|
|
|
11649
12124
|
requires: [GCP_SQL_CHECKS.instances],
|
|
11650
12125
|
evaluate: sqlSinCmekEvaluate
|
|
11651
12126
|
};
|
|
12127
|
+
var SOLO_CIFRADO = /* @__PURE__ */ new Set(["ENCRYPTED_ONLY", "TRUSTED_CLIENT_CERTIFICATE_REQUIRED"]);
|
|
12128
|
+
function sqlSinSslEvaluate(observation) {
|
|
12129
|
+
const modo = observation.attributes.sslMode;
|
|
12130
|
+
if (typeof modo === "string" && modo.length > 0) {
|
|
12131
|
+
if (SOLO_CIFRADO.has(modo)) return { status: "ok" };
|
|
12132
|
+
return { status: "finding", evidence: { sslMode: modo, ipv4Enabled: observation.attributes.ipv4Enabled ?? null } };
|
|
12133
|
+
}
|
|
12134
|
+
const requiere = observation.attributes.requireSsl;
|
|
12135
|
+
if (requiere === true) return { status: "ok" };
|
|
12136
|
+
if (!("sslMode" in observation.attributes)) {
|
|
12137
|
+
return { status: "unknown", check: GCP_SQL_CHECKS.instances, attribute: "sslMode" };
|
|
12138
|
+
}
|
|
12139
|
+
if (requiere !== false && requiere !== null) {
|
|
12140
|
+
return { status: "unknown", check: GCP_SQL_CHECKS.instances, attribute: "requireSsl" };
|
|
12141
|
+
}
|
|
12142
|
+
return { status: "finding", evidence: { sslMode: null, requireSsl: requiere, ipv4Enabled: observation.attributes.ipv4Enabled ?? null } };
|
|
12143
|
+
}
|
|
12144
|
+
var sqlSinSsl = {
|
|
12145
|
+
id: "SEC_GCP_ENCRYPTION_SQL_SIN_SSL",
|
|
12146
|
+
scope: "resource",
|
|
12147
|
+
category: "security_encryption",
|
|
12148
|
+
severity: "medium",
|
|
12149
|
+
provider: "gcp",
|
|
12150
|
+
kinds: ["cloud_sql_instance"],
|
|
12151
|
+
requires: [GCP_SQL_CHECKS.instances],
|
|
12152
|
+
evaluate: sqlSinSslEvaluate
|
|
12153
|
+
};
|
|
12154
|
+
var KMS_MAX_ROTATION_DAYS = 90;
|
|
12155
|
+
function segundos(periodo) {
|
|
12156
|
+
const m = /^(\d+(?:\.\d+)?)s$/.exec(periodo);
|
|
12157
|
+
return m ? Number(m[1]) : void 0;
|
|
12158
|
+
}
|
|
12159
|
+
function kmsSinRotacionEvaluate2(observation) {
|
|
12160
|
+
const a = observation.attributes;
|
|
12161
|
+
if (!("purpose" in a) || !("primaryState" in a) || !("rotationPeriod" in a)) {
|
|
12162
|
+
return { status: "unknown", check: GCP_KMS_CHECKS.cryptoKeys, attribute: "rotationPeriod" };
|
|
12163
|
+
}
|
|
12164
|
+
if (a.purpose !== "ENCRYPT_DECRYPT" || a.primaryState !== "ENABLED") return { status: "ok" };
|
|
12165
|
+
if (a.importOnly === true || typeof a.protectionLevel === "string" && a.protectionLevel.startsWith("EXTERNAL")) {
|
|
12166
|
+
return { status: "ok" };
|
|
12167
|
+
}
|
|
12168
|
+
if (a.rotationPeriod === null) return { status: "finding", evidence: { rotationPeriod: null } };
|
|
12169
|
+
const s = typeof a.rotationPeriod === "string" ? segundos(a.rotationPeriod) : void 0;
|
|
12170
|
+
if (s === void 0) return { status: "unknown", check: GCP_KMS_CHECKS.cryptoKeys, attribute: "rotationPeriod" };
|
|
12171
|
+
if (s <= KMS_MAX_ROTATION_DAYS * 86400) return { status: "ok" };
|
|
12172
|
+
return { status: "finding", evidence: { rotationPeriodDays: Math.round(s / 86400) } };
|
|
12173
|
+
}
|
|
12174
|
+
var kmsSinRotacion2 = {
|
|
12175
|
+
id: "SEC_GCP_ENCRYPTION_KMS_SIN_ROTACION",
|
|
12176
|
+
scope: "resource",
|
|
12177
|
+
category: "security_encryption",
|
|
12178
|
+
severity: "medium",
|
|
12179
|
+
provider: "gcp",
|
|
12180
|
+
kinds: ["kms_key"],
|
|
12181
|
+
requires: [GCP_KMS_CHECKS.locations, GCP_KMS_CHECKS.keyRings, GCP_KMS_CHECKS.cryptoKeys],
|
|
12182
|
+
evaluate: kmsSinRotacionEvaluate2
|
|
12183
|
+
};
|
|
11652
12184
|
|
|
11653
12185
|
// ../../services/security/src/rules/gcp/exposure.ts
|
|
11654
12186
|
function evaluate7(observation) {
|
|
@@ -11884,9 +12416,42 @@ var sinExportSink = {
|
|
|
11884
12416
|
requires: [GCP_LOGGING_CHECKS.sinks],
|
|
11885
12417
|
evaluate: evaluate8
|
|
11886
12418
|
};
|
|
12419
|
+
function vpcSinFlowLogsEvaluate(observation) {
|
|
12420
|
+
const subnets = observation.attributes.subnets;
|
|
12421
|
+
if (!Array.isArray(subnets)) {
|
|
12422
|
+
return { status: "unknown", check: GCP_COMPUTE_CHECKS.subnetworks, attribute: "subnets" };
|
|
12423
|
+
}
|
|
12424
|
+
const lista = subnets;
|
|
12425
|
+
const sinRegistro = lista.filter((subnet) => subnet.covered === false);
|
|
12426
|
+
if (sinRegistro.length > 0) {
|
|
12427
|
+
return {
|
|
12428
|
+
status: "finding",
|
|
12429
|
+
evidence: { unloggedSubnets: sinRegistro.map((subnet) => `${String(subnet.region)}/${String(subnet.name)}`), subnetCount: lista.length }
|
|
12430
|
+
};
|
|
12431
|
+
}
|
|
12432
|
+
if (lista.some((subnet) => subnet.covered !== true)) {
|
|
12433
|
+
return { status: "unknown", check: GCP_NETWORK_MANAGEMENT_CHECKS.vpcFlowLogsConfigs, attribute: "subnets" };
|
|
12434
|
+
}
|
|
12435
|
+
if (observation.attributes.subnetsComplete !== true) {
|
|
12436
|
+
return { status: "unknown", check: GCP_COMPUTE_CHECKS.subnetworks, attribute: "subnets" };
|
|
12437
|
+
}
|
|
12438
|
+
return { status: "ok" };
|
|
12439
|
+
}
|
|
12440
|
+
var vpcSinFlowLogs = {
|
|
12441
|
+
id: "SEC_GCP_LOGGING_VPC_SIN_FLOW_LOGS",
|
|
12442
|
+
scope: "resource",
|
|
12443
|
+
category: "security_logging",
|
|
12444
|
+
severity: "medium",
|
|
12445
|
+
provider: "gcp",
|
|
12446
|
+
kinds: ["vpc_network"],
|
|
12447
|
+
// Las configs de Network Management no van aquí: una red con `logConfig` en cada subred ya está bien sin ellas.
|
|
12448
|
+
requires: [GCP_COMPUTE_CHECKS.subnetworks],
|
|
12449
|
+
evaluate: vpcSinFlowLogsEvaluate
|
|
12450
|
+
};
|
|
11887
12451
|
|
|
11888
12452
|
// ../../services/security/src/rules/gcp/network.ts
|
|
11889
|
-
|
|
12453
|
+
var evaluate9 = (observation) => firewallAbierto(observation, adminPortsReached);
|
|
12454
|
+
function firewallAbierto(observation, reached) {
|
|
11890
12455
|
const direction = observation.attributes.direction;
|
|
11891
12456
|
const disabled = observation.attributes.disabled;
|
|
11892
12457
|
const sources = observation.attributes.sourceRanges;
|
|
@@ -11905,7 +12470,7 @@ function evaluate9(observation) {
|
|
|
11905
12470
|
if (typeof entry !== "object" || entry === null) continue;
|
|
11906
12471
|
const rule = entry;
|
|
11907
12472
|
const protocol = typeof rule.protocol === "string" ? rule.protocol : "";
|
|
11908
|
-
for (const puerto of
|
|
12473
|
+
for (const puerto of reached(protocol, cadenas2(rule.ports))) puertos.add(puerto);
|
|
11909
12474
|
}
|
|
11910
12475
|
if (puertos.size === 0) return { status: "ok" };
|
|
11911
12476
|
return {
|
|
@@ -12065,6 +12630,16 @@ var firewallAdminAbierto = {
|
|
|
12065
12630
|
requires: [GCP_COMPUTE_CHECKS.firewalls],
|
|
12066
12631
|
evaluate: evaluate9
|
|
12067
12632
|
};
|
|
12633
|
+
var firewallBdAbierto = {
|
|
12634
|
+
id: "SEC_GCP_NETWORK_BD_ABIERTO",
|
|
12635
|
+
scope: "resource",
|
|
12636
|
+
category: "security_network",
|
|
12637
|
+
severity: "high",
|
|
12638
|
+
provider: "gcp",
|
|
12639
|
+
kinds: ["firewall"],
|
|
12640
|
+
requires: [GCP_COMPUTE_CHECKS.firewalls],
|
|
12641
|
+
evaluate: (observation) => firewallAbierto(observation, databasePortsReached)
|
|
12642
|
+
};
|
|
12068
12643
|
|
|
12069
12644
|
// ../../services/security/src/rules/gcp/secrets.ts
|
|
12070
12645
|
var SOSPECHOSAS2 = ["PASSWORD", "PASSWD", "SECRET", "TOKEN", "APIKEY", "API_KEY", "PRIVATE_KEY", "CREDENTIAL"];
|
|
@@ -12099,11 +12674,14 @@ var secretoEnFunction = {
|
|
|
12099
12674
|
// ../../services/security/src/rules/gcp/index.ts
|
|
12100
12675
|
var GCP_SECURITY_RULES = [
|
|
12101
12676
|
firewallAdminAbierto,
|
|
12677
|
+
firewallBdAbierto,
|
|
12102
12678
|
politicaAdminAbierta,
|
|
12103
12679
|
sqlPublico2,
|
|
12104
12680
|
gcsPublico,
|
|
12105
12681
|
discoSinCmek,
|
|
12106
12682
|
sqlSinCmek,
|
|
12683
|
+
sqlSinSsl,
|
|
12684
|
+
kmsSinRotacion2,
|
|
12107
12685
|
secretoEnFunction,
|
|
12108
12686
|
rolPrimitivo,
|
|
12109
12687
|
claveSaUserManaged,
|
|
@@ -12111,7 +12689,8 @@ var GCP_SECURITY_RULES = [
|
|
|
12111
12689
|
claveSaSinUso,
|
|
12112
12690
|
permisosSinUso2,
|
|
12113
12691
|
auditIncompleto,
|
|
12114
|
-
sinExportSink
|
|
12692
|
+
sinExportSink,
|
|
12693
|
+
vpcSinFlowLogs
|
|
12115
12694
|
];
|
|
12116
12695
|
|
|
12117
12696
|
// src/security/evaluate.ts
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aaa-sentinel/iac-cost",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "Estimate the monthly cost delta of a Terraform plan, check it against the AAA Sentinel security rules, open security fixes as pull requests and report how the security posture changed, from your CI",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|