@aaa-sentinel/iac-cost 0.3.1 → 0.4.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 +64 -5
- package/dist/cli.js +682 -116
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,7 @@ var FORMATS = ["markdown", "text", "json"];
|
|
|
14
14
|
var SEVERITIES = ["critical", "high", "medium", "low"];
|
|
15
15
|
var USAGE = `Usage: iac-cost [plan.json] [options]
|
|
16
16
|
iac-cost fix [plan.json] --account <accountId> [options] (see iac-cost fix --help)
|
|
17
|
+
iac-cost posture --account <accountId> [options] (see iac-cost posture --help)
|
|
17
18
|
|
|
18
19
|
Reads \`terraform show -json\` output from the file or from stdin.
|
|
19
20
|
|
|
@@ -1724,9 +1725,9 @@ var Doc = class {
|
|
|
1724
1725
|
return;
|
|
1725
1726
|
}
|
|
1726
1727
|
const content = arg;
|
|
1727
|
-
const
|
|
1728
|
-
const minIndent = Math.min(...
|
|
1729
|
-
const dedented =
|
|
1728
|
+
const lines2 = content.split("\n").filter((x) => x);
|
|
1729
|
+
const minIndent = Math.min(...lines2.map((x) => x.length - x.trimStart().length));
|
|
1730
|
+
const dedented = lines2.map((x) => x.slice(minIndent)).map((x) => " ".repeat(this.indent * 2) + x);
|
|
1730
1731
|
for (const line of dedented) {
|
|
1731
1732
|
this.content.push(line);
|
|
1732
1733
|
}
|
|
@@ -6409,8 +6410,8 @@ function aggregateEstimates(estimates) {
|
|
|
6409
6410
|
const billable = pricedResources + unpricedResources;
|
|
6410
6411
|
const ratio = billable === 0 ? 1 : pricedResources / billable;
|
|
6411
6412
|
const sufficientForTotal = ratio >= IAC_MIN_COVERAGE_RATIO;
|
|
6412
|
-
const
|
|
6413
|
-
const totalMonthlyDeltaUsd = sufficientForTotal ? round2(
|
|
6413
|
+
const total2 = [...priced.values()].reduce((acc, i) => acc + i.monthlyDeltaUsd, 0);
|
|
6414
|
+
const totalMonthlyDeltaUsd = sufficientForTotal ? round2(total2) : null;
|
|
6414
6415
|
const first = estimates[0]?.displayCurrency;
|
|
6415
6416
|
const display = first && estimates.every((e) => e.displayCurrency?.code === first.code && e.displayCurrency.rate === first.rate) ? first : null;
|
|
6416
6417
|
for (const item of priced.values()) {
|
|
@@ -8152,6 +8153,8 @@ function sqlInstances(index) {
|
|
|
8152
8153
|
const builder = new ObservationBuilder(index, sql, "cloud_sql_instance", "gcp").cloudId("name");
|
|
8153
8154
|
const ip = ["settings", 0, "ip_configuration", 0];
|
|
8154
8155
|
builder.from("ipv4Enabled", index.get(sql, ...ip, "ipv4_enabled"), Boolean, true);
|
|
8156
|
+
builder.from("sslMode", index.get(sql, ...ip, "ssl_mode"), String, null);
|
|
8157
|
+
builder.from("requireSsl", index.get(sql, ...ip, "require_ssl"), Boolean, null);
|
|
8155
8158
|
const networks = index.get(sql, ...ip, "authorized_networks");
|
|
8156
8159
|
if (networks.state === "unknown") builder.gap("authorizedNetworks", "known_after_apply");
|
|
8157
8160
|
else {
|
|
@@ -8267,6 +8270,18 @@ var RULE_TITLES = {
|
|
|
8267
8270
|
SEC_AWS_NETWORK_ADMIN_PREFIX_LIST: "Admin ports open to the internet through a prefix list",
|
|
8268
8271
|
SEC_AWS_EXPOSURE_S3_PUBLICO: "Public S3 bucket",
|
|
8269
8272
|
SEC_AWS_EXPOSURE_LAMBDA_PUBLICA: "Lambda invocable from the internet",
|
|
8273
|
+
SEC_AZURE_ENCRYPTION_KEYVAULT_SIN_PURGA: "Key Vault without purge protection",
|
|
8274
|
+
SEC_AZURE_ENCRYPTION_SQL_SIN_TDE: "SQL database without TDE encryption",
|
|
8275
|
+
SEC_AZURE_LOGGING_VNET_SIN_FLOW_LOGS: "VNet without flow logs",
|
|
8276
|
+
SEC_GCP_ENCRYPTION_KMS_SIN_ROTACION: "Cloud KMS key without 90-day rotation",
|
|
8277
|
+
SEC_GCP_LOGGING_VPC_SIN_FLOW_LOGS: "VPC network with subnets without flow logs",
|
|
8278
|
+
SEC_AWS_NETWORK_BD_ABIERTO: "Database ports open to the internet",
|
|
8279
|
+
SEC_AZURE_NETWORK_BD_ABIERTO: "NSG with database ports open to the internet",
|
|
8280
|
+
SEC_GCP_NETWORK_BD_ABIERTO: "Firewall with database ports open to the internet",
|
|
8281
|
+
SEC_AWS_EXPOSURE_RDS_PUBLICA: "Publicly accessible RDS instance",
|
|
8282
|
+
SEC_GCP_ENCRYPTION_SQL_SIN_SSL: "Cloud SQL accepts unencrypted connections",
|
|
8283
|
+
SEC_AWS_IAM_CLAVE_ANTIGUA: "Active access key older than 90 days",
|
|
8284
|
+
SEC_AWS_IAM_POLITICA_CONTRASENAS: "Weak or missing IAM password policy",
|
|
8270
8285
|
SEC_AWS_ENCRYPTION_KMS_SIN_ROTACION: "KMS key without rotation",
|
|
8271
8286
|
SEC_AWS_ENCRYPTION_RDS_SIN_CIFRAR: "Unencrypted RDS",
|
|
8272
8287
|
SEC_AWS_LOGGING_TRAIL_PARADO: "CloudTrail stopped",
|
|
@@ -8285,6 +8300,7 @@ var RULE_TITLES = {
|
|
|
8285
8300
|
SEC_AZURE_LOGGING_SIN_DIAGNOSTICO: "Subscription does not export Activity Log",
|
|
8286
8301
|
SEC_GCP_NETWORK_ADMIN_ABIERTO: "Firewall with admin ports open to the internet",
|
|
8287
8302
|
SEC_GCP_EXPOSURE_SQL_PUBLICO: "Cloud SQL authorized from 0.0.0.0/0",
|
|
8303
|
+
SEC_GCP_EXPOSURE_GCS_PUBLICO: "Public Cloud Storage bucket (allUsers or allAuthenticatedUsers)",
|
|
8288
8304
|
SEC_GCP_ENCRYPTION_SQL_SIN_CMEK: "Cloud SQL without CMEK",
|
|
8289
8305
|
SEC_GCP_SECRETS_FUNCTION_ENV: "Secret in a Cloud Function environment variable",
|
|
8290
8306
|
SEC_GCP_IAM_ROL_PRIMITIVO: "Primitive Owner or Editor role on the project",
|
|
@@ -8779,7 +8795,7 @@ function carry(findingId, fix2, state) {
|
|
|
8779
8795
|
}
|
|
8780
8796
|
async function runFix(options, deps) {
|
|
8781
8797
|
const warnings = [];
|
|
8782
|
-
const
|
|
8798
|
+
const lines2 = [];
|
|
8783
8799
|
const reports = [];
|
|
8784
8800
|
const { host } = deps;
|
|
8785
8801
|
let input;
|
|
@@ -8787,7 +8803,7 @@ async function runFix(options, deps) {
|
|
|
8787
8803
|
input = await deps.api.load();
|
|
8788
8804
|
} catch (error2) {
|
|
8789
8805
|
warnings.push(`could not read the open findings, nothing to fix: ${error2.message}`);
|
|
8790
|
-
return { stdout: render(options,
|
|
8806
|
+
return { stdout: render(options, lines2, deps.accountId, null), warnings, lines: lines2, reported: 0 };
|
|
8791
8807
|
}
|
|
8792
8808
|
if (input.truncated) warnings.push("the account has more open findings than the API returns (500 most severe): the rest are left for a later run");
|
|
8793
8809
|
let publicRepo = false;
|
|
@@ -8818,7 +8834,7 @@ async function runFix(options, deps) {
|
|
|
8818
8834
|
}
|
|
8819
8835
|
if (state !== "merged") {
|
|
8820
8836
|
reports.push(carry(finding.findingId, existing, state));
|
|
8821
|
-
|
|
8837
|
+
lines2.push({ findingId: finding.findingId, ruleId: finding.ruleId, address: existing.address, outcome: state, pullRequest: existing.pullRequest.url });
|
|
8822
8838
|
continue;
|
|
8823
8839
|
}
|
|
8824
8840
|
existing.state = "merged";
|
|
@@ -8840,7 +8856,7 @@ async function runFix(options, deps) {
|
|
|
8840
8856
|
const merged = existing && sameRepo && existing.state === "merged" && existing.pullRequest ? existing : null;
|
|
8841
8857
|
if (merged && outcome.kind !== "patch") {
|
|
8842
8858
|
reports.push(carry(finding.findingId, merged, "merged"));
|
|
8843
|
-
|
|
8859
|
+
lines2.push({ findingId: finding.findingId, ruleId: finding.ruleId, address: merged.address, outcome: "merged", ...merged.pullRequest ? { pullRequest: merged.pullRequest.url } : {} });
|
|
8844
8860
|
continue;
|
|
8845
8861
|
}
|
|
8846
8862
|
const base = { findingId: finding.findingId, ruleId: finding.ruleId, address: outcome.address };
|
|
@@ -8853,18 +8869,18 @@ async function runFix(options, deps) {
|
|
|
8853
8869
|
...outcome.file !== void 0 ? { file: outcome.file } : {},
|
|
8854
8870
|
...outcome.attribute !== void 0 ? { attribute: outcome.attribute } : {}
|
|
8855
8871
|
});
|
|
8856
|
-
|
|
8872
|
+
lines2.push({ ...base, outcome: "suggested", reason: outcome.reason, ...outcome.file !== void 0 ? { file: outcome.file } : {} });
|
|
8857
8873
|
continue;
|
|
8858
8874
|
}
|
|
8859
8875
|
if (outcome.kind === "fixed_in_code") {
|
|
8860
8876
|
reports.push({ findingId: finding.findingId, state: "pending", reason: "fixed_in_code", address: outcome.address, file: outcome.file, attribute: outcome.attribute });
|
|
8861
|
-
|
|
8877
|
+
lines2.push({ ...base, outcome: "pending", reason: "fixed_in_code", file: outcome.file });
|
|
8862
8878
|
continue;
|
|
8863
8879
|
}
|
|
8864
8880
|
const patch = outcome;
|
|
8865
8881
|
const pending = (reason) => {
|
|
8866
8882
|
reports.push({ findingId: finding.findingId, state: "pending", reason, address: patch.address, file: patch.file, attribute: patch.attribute });
|
|
8867
|
-
|
|
8883
|
+
lines2.push({ ...base, outcome: "pending", reason, file: patch.file, changes: patch.changes });
|
|
8868
8884
|
};
|
|
8869
8885
|
if (options.dryRun || !host) {
|
|
8870
8886
|
pending("dry_run");
|
|
@@ -8872,7 +8888,7 @@ async function runFix(options, deps) {
|
|
|
8872
8888
|
}
|
|
8873
8889
|
if (host.unsupported) {
|
|
8874
8890
|
reports.push({ findingId: finding.findingId, state: "suggested", reason: "unsupported_host", address: patch.address, file: patch.file, attribute: patch.attribute });
|
|
8875
|
-
|
|
8891
|
+
lines2.push({ ...base, outcome: "suggested", reason: "unsupported_host", file: patch.file, changes: patch.changes });
|
|
8876
8892
|
continue;
|
|
8877
8893
|
}
|
|
8878
8894
|
if (publicRepo) {
|
|
@@ -8896,7 +8912,7 @@ async function runFix(options, deps) {
|
|
|
8896
8912
|
repository: host.repository,
|
|
8897
8913
|
pullRequest: { url: pr.url, number: pr.number }
|
|
8898
8914
|
});
|
|
8899
|
-
|
|
8915
|
+
lines2.push({ ...base, outcome: state, file: patch.file, pullRequest: pr.url, changes: patch.changes });
|
|
8900
8916
|
};
|
|
8901
8917
|
try {
|
|
8902
8918
|
if (await host.branchExists(branch)) {
|
|
@@ -8946,7 +8962,7 @@ async function runFix(options, deps) {
|
|
|
8946
8962
|
warnings.push(`could not record the fixes in AAA Sentinel: ${error2.message}`);
|
|
8947
8963
|
}
|
|
8948
8964
|
}
|
|
8949
|
-
return { stdout: render(options,
|
|
8965
|
+
return { stdout: render(options, lines2, deps.accountId, input.scanAt), warnings, lines: lines2, reported };
|
|
8950
8966
|
}
|
|
8951
8967
|
var SkipFinding = class extends Error {
|
|
8952
8968
|
};
|
|
@@ -8965,11 +8981,11 @@ var OUTCOME_TEXT = {
|
|
|
8965
8981
|
pending: "pending",
|
|
8966
8982
|
suggested: "change suggested, no pull request"
|
|
8967
8983
|
};
|
|
8968
|
-
function render(options,
|
|
8969
|
-
if (options.format === "json") return JSON.stringify({ formatVersion: "1", accountId, scanAt, dryRun: options.dryRun, fixes:
|
|
8970
|
-
if (
|
|
8984
|
+
function render(options, lines2, accountId, scanAt) {
|
|
8985
|
+
if (options.format === "json") return JSON.stringify({ formatVersion: "1", accountId, scanAt, dryRun: options.dryRun, fixes: lines2 }, null, 2);
|
|
8986
|
+
if (lines2.length === 0) return `No open finding of account ${accountId} matches a resource of this plan.`;
|
|
8971
8987
|
const out = [`Security fixes for account ${accountId}${options.dryRun ? " (dry run: nothing opened or recorded)" : ""}`];
|
|
8972
|
-
for (const line of
|
|
8988
|
+
for (const line of lines2) {
|
|
8973
8989
|
const detail = [line.reason, line.file, line.pullRequest].filter(Boolean).join(", ");
|
|
8974
8990
|
out.push(` ${line.address} \u2014 ${title(line.ruleId)}: ${OUTCOME_TEXT[line.outcome]}${detail ? ` (${detail})` : ""}`);
|
|
8975
8991
|
for (const change of line.changes ?? []) out.push(` ${change}`);
|
|
@@ -9078,6 +9094,168 @@ ${FIX_USAGE}`);
|
|
|
9078
9094
|
return { stdout: result.stdout, warnings: [...warnings, ...result.warnings], exitCode: 0 };
|
|
9079
9095
|
}
|
|
9080
9096
|
|
|
9097
|
+
// src/posture/posture.ts
|
|
9098
|
+
import { parseArgs as parseArgs3 } from "node:util";
|
|
9099
|
+
var POSTURE_USAGE = `Usage: iac-cost posture --account <accountId> [options]
|
|
9100
|
+
|
|
9101
|
+
Prints how the security posture of an AAA Sentinel account changed over a
|
|
9102
|
+
period: Score from the first to the last day with a scan, distinct findings
|
|
9103
|
+
resolved and new, mean time to fix, and catalog or coverage changes that make
|
|
9104
|
+
the Score not comparable.
|
|
9105
|
+
|
|
9106
|
+
Options:
|
|
9107
|
+
--account <accountId> AAA Sentinel account id (acc_\u2026), or IAC_SECURITY_ACCOUNT_ID
|
|
9108
|
+
--days <30|90|365> Period (default: 90)
|
|
9109
|
+
--format <markdown|text|json>
|
|
9110
|
+
Output format (default: markdown)
|
|
9111
|
+
--fail-on-new <critical|high|medium|low>
|
|
9112
|
+
Exit 1 when new findings at this severity or worse
|
|
9113
|
+
appeared in the period (findings that cannot be
|
|
9114
|
+
compared with the previous scan do not count).
|
|
9115
|
+
With the gate, a 4xx from the API (scope, account)
|
|
9116
|
+
exits 2; our outage still exits 0.
|
|
9117
|
+
-h, --help Show this help
|
|
9118
|
+
|
|
9119
|
+
Environment:
|
|
9120
|
+
IAC_SECURITY_CLIENT_ID / IAC_SECURITY_CLIENT_SECRET OAuth2 app with the security:read scope`;
|
|
9121
|
+
var SEVERITIES2 = ["critical", "high", "medium", "low"];
|
|
9122
|
+
var DAYS = [30, 90, 365];
|
|
9123
|
+
var FORMATS2 = ["markdown", "text", "json"];
|
|
9124
|
+
var TIMEOUT_MS3 = 35e3;
|
|
9125
|
+
var APP_URL = "https://app.aaa-sentinel.com";
|
|
9126
|
+
var total = (c) => SEVERITIES2.reduce((n, s) => n + (c[s] ?? 0), 0);
|
|
9127
|
+
var bySeverity = (c) => SEVERITIES2.filter((s) => (c[s] ?? 0) > 0).map((s) => `${c[s]} ${s}`).join(", ");
|
|
9128
|
+
var REASONS = {
|
|
9129
|
+
catalog_changed: "the rule catalog changed",
|
|
9130
|
+
coverage_changed: "what could be evaluated changed",
|
|
9131
|
+
not_compared: "a scan could not be compared with the previous one",
|
|
9132
|
+
scored_changed: "the number of scoring rules changed",
|
|
9133
|
+
partial: "one end was read incompletely"
|
|
9134
|
+
};
|
|
9135
|
+
function scoreLine(summary) {
|
|
9136
|
+
const from = summary.from?.score;
|
|
9137
|
+
const to = summary.to?.score;
|
|
9138
|
+
if (from === null || from === void 0 || to === null || to === void 0) return "no comparable Score in the period";
|
|
9139
|
+
if (!summary.comparable) {
|
|
9140
|
+
const why = summary.notComparableReasons.map((r) => REASONS[r] ?? r).join("; ");
|
|
9141
|
+
return `${from} \u2192 ${to}, not comparable (${why})`;
|
|
9142
|
+
}
|
|
9143
|
+
if (to > from) return `up from ${from} to ${to}`;
|
|
9144
|
+
if (to < from) return `down from ${from} to ${to}`;
|
|
9145
|
+
return `steady at ${to}`;
|
|
9146
|
+
}
|
|
9147
|
+
function lines(history) {
|
|
9148
|
+
const s = history.summary;
|
|
9149
|
+
const fix2 = ["critical", "high"].filter((sev) => s.meanFixDays[sev] !== null).map((sev) => `${sev} ${s.meanFixDays[sev]} d`);
|
|
9150
|
+
const out = [
|
|
9151
|
+
["Score", scoreLine(s)],
|
|
9152
|
+
["Resolved", `${total(s.resolved)}${total(s.resolved) ? ` (${bySeverity(s.resolved)})` : ""}`],
|
|
9153
|
+
["New", `${total(s.opened)}${total(s.opened) ? ` (${bySeverity(s.opened)})` : ""}`]
|
|
9154
|
+
];
|
|
9155
|
+
if (total(s.surfaced)) out.push(["Not comparable", `${total(s.surfaced)} new findings whose rule was not fully evaluated before`]);
|
|
9156
|
+
if (total(s.unverified)) out.push(["Unverified", `${total(s.unverified)} gone without confirmation`]);
|
|
9157
|
+
out.push(["Mean time to fix", fix2.length ? fix2.join(", ") : "no critical or high finding fixed with a known date"]);
|
|
9158
|
+
out.push(["Scans", String(s.scans)]);
|
|
9159
|
+
return out;
|
|
9160
|
+
}
|
|
9161
|
+
function renderPosture(history, format) {
|
|
9162
|
+
const enlace = `${APP_URL}/posture?accountId=${encodeURIComponent(history.accountId)}`;
|
|
9163
|
+
const notas = [];
|
|
9164
|
+
if (history.points.length < 2) notas.push("The trend appears once the period has scans on at least two different days.");
|
|
9165
|
+
if (history.acceptedRisks) notas.push("The account has accepted risks: each day keeps the Score computed at the time.");
|
|
9166
|
+
if (history.summary.notCompared) notas.push(`${history.summary.notCompared} scan(s) could not be compared with the previous one.`);
|
|
9167
|
+
if (history.summary.changesTruncated) notas.push("A scan had more changes than are stored: the figures are a minimum.");
|
|
9168
|
+
if (history.truncated) notas.push("The period has more scans than fit: only the most recent ones are counted.");
|
|
9169
|
+
const titulo = `Security posture, last ${history.days} days`;
|
|
9170
|
+
if (format === "text") {
|
|
9171
|
+
return [titulo, ...lines(history).map(([k, v]) => ` ${k}: ${v}`), ...notas.map((n) => ` ${n}`), ` ${enlace}`].join("\n");
|
|
9172
|
+
}
|
|
9173
|
+
return [
|
|
9174
|
+
`### ${titulo}`,
|
|
9175
|
+
"",
|
|
9176
|
+
"| | |",
|
|
9177
|
+
"|---|---|",
|
|
9178
|
+
...lines(history).map(([k, v]) => `| ${k} | ${v} |`),
|
|
9179
|
+
"",
|
|
9180
|
+
...notas.map((n) => `> ${n}`),
|
|
9181
|
+
...notas.length ? [""] : [],
|
|
9182
|
+
`[Open in AAA Sentinel](${enlace})`
|
|
9183
|
+
].join("\n");
|
|
9184
|
+
}
|
|
9185
|
+
function newAtOrAbove(summary, severity) {
|
|
9186
|
+
return SEVERITIES2.slice(0, SEVERITIES2.indexOf(severity) + 1).reduce((n, s) => n + (summary.opened[s] ?? 0), 0);
|
|
9187
|
+
}
|
|
9188
|
+
async function fetchPostureHistory(config2) {
|
|
9189
|
+
const fetchImpl = config2.fetchImpl ?? fetch;
|
|
9190
|
+
const token = await fetchToken(config2, "security:read");
|
|
9191
|
+
const url = `${config2.baseUrl}/v1/security/posture-history?accountId=${encodeURIComponent(config2.accountId)}&days=${config2.days}`;
|
|
9192
|
+
const response = await fetchImpl(url, { headers: { authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(TIMEOUT_MS3) });
|
|
9193
|
+
if (!response.ok) throw new Error(`posture_history_http_${response.status}`);
|
|
9194
|
+
return await response.json();
|
|
9195
|
+
}
|
|
9196
|
+
async function runPostureCli(argv, io) {
|
|
9197
|
+
const warnings = [];
|
|
9198
|
+
const fail = (message) => ({ stdout: null, warnings: [...warnings, message], exitCode: 2 });
|
|
9199
|
+
const { values, positionals } = parseArgs3({
|
|
9200
|
+
args: [...argv],
|
|
9201
|
+
strict: false,
|
|
9202
|
+
allowPositionals: true,
|
|
9203
|
+
options: {
|
|
9204
|
+
account: { type: "string" },
|
|
9205
|
+
days: { type: "string" },
|
|
9206
|
+
format: { type: "string" },
|
|
9207
|
+
"fail-on-new": { type: "string" },
|
|
9208
|
+
help: { type: "boolean", short: "h" }
|
|
9209
|
+
}
|
|
9210
|
+
});
|
|
9211
|
+
const known = /* @__PURE__ */ new Set(["account", "days", "format", "fail-on-new", "help", "h"]);
|
|
9212
|
+
for (const name of Object.keys(values)) {
|
|
9213
|
+
if (known.has(name)) continue;
|
|
9214
|
+
if (/^fail[-_]?on/i.test(name)) return fail(`unknown option --${name}: the gate is spelled --fail-on-new`);
|
|
9215
|
+
warnings.push(`unknown option --${name} ignored`);
|
|
9216
|
+
}
|
|
9217
|
+
if (values.help === true || values.h === true) return { stdout: POSTURE_USAGE, warnings, exitCode: 0 };
|
|
9218
|
+
for (const name of ["account", "days", "format", "fail-on-new"]) {
|
|
9219
|
+
const v = values[name];
|
|
9220
|
+
if (v === true || typeof v === "string" && v.startsWith("-")) return fail(`--${name} needs a value`);
|
|
9221
|
+
}
|
|
9222
|
+
if (positionals.length > 0) return fail(`unexpected argument ${positionals.join(" ")}`);
|
|
9223
|
+
const account = (typeof values.account === "string" ? values.account : io.env["IAC_SECURITY_ACCOUNT_ID"])?.trim();
|
|
9224
|
+
if (!account) return fail(`posture needs --account <accountId>
|
|
9225
|
+
|
|
9226
|
+
${POSTURE_USAGE}`);
|
|
9227
|
+
const days = values.days === void 0 ? 90 : Number(values.days);
|
|
9228
|
+
if (!DAYS.includes(days)) return fail("--days must be 30, 90 or 365");
|
|
9229
|
+
const format = values.format === void 0 ? "markdown" : values.format;
|
|
9230
|
+
if (typeof format !== "string" || !FORMATS2.includes(format)) return fail(`--format must be one of ${FORMATS2.join(", ")}`);
|
|
9231
|
+
let gate = null;
|
|
9232
|
+
if (values["fail-on-new"] !== void 0) {
|
|
9233
|
+
const raw = typeof values["fail-on-new"] === "string" ? values["fail-on-new"].trim().toLowerCase() : "";
|
|
9234
|
+
if (!SEVERITIES2.includes(raw)) return fail(`--fail-on-new must be one of ${SEVERITIES2.join(", ")}`);
|
|
9235
|
+
gate = raw;
|
|
9236
|
+
}
|
|
9237
|
+
const baseUrl = resolveBaseUrl(io.env["IAC_COST_BASE_URL"] ?? "https://api.aaa-sentinel.com");
|
|
9238
|
+
if (baseUrl === null) return fail("IAC_COST_BASE_URL must be an https URL");
|
|
9239
|
+
const clientId = io.env["IAC_SECURITY_CLIENT_ID"];
|
|
9240
|
+
const clientSecret = io.env["IAC_SECURITY_CLIENT_SECRET"];
|
|
9241
|
+
if (!clientId || !clientSecret) return fail("IAC_SECURITY_CLIENT_ID/IAC_SECURITY_CLIENT_SECRET are not set: create an app with the security:read scope");
|
|
9242
|
+
let history;
|
|
9243
|
+
try {
|
|
9244
|
+
history = await fetchPostureHistory({ baseUrl, clientId, clientSecret, accountId: account, days, ...io.fetchImpl ? { fetchImpl: io.fetchImpl } : {} });
|
|
9245
|
+
} catch (error2) {
|
|
9246
|
+
const mensaje = error2.message;
|
|
9247
|
+
if (gate && /_http_4(00|01|03|04)$/.test(mensaje)) return fail(`could not read the posture history (${mensaje}): check the app scope (security:read) and the account id`);
|
|
9248
|
+
return { stdout: null, warnings: [...warnings, `could not read the posture history, skipping: ${mensaje}`], exitCode: 0 };
|
|
9249
|
+
}
|
|
9250
|
+
if (history.access !== "full") {
|
|
9251
|
+
return { stdout: null, warnings: [...warnings, "the account does not have the security add-on: no posture history to show"], exitCode: 0 };
|
|
9252
|
+
}
|
|
9253
|
+
const nuevos = gate ? newAtOrAbove(history.summary, gate) : 0;
|
|
9254
|
+
const stdout = format === "json" ? JSON.stringify(history, null, 2) : renderPosture(history, format);
|
|
9255
|
+
if (gate && nuevos > 0) warnings.push(`${nuevos} new finding(s) at ${gate} or worse in the last ${days} days (--fail-on-new ${gate})`);
|
|
9256
|
+
return { stdout, warnings, exitCode: gate && nuevos > 0 ? 1 : 0 };
|
|
9257
|
+
}
|
|
9258
|
+
|
|
9081
9259
|
// src/terraform-plan.ts
|
|
9082
9260
|
function actionOf2(actions) {
|
|
9083
9261
|
if (actions.includes("create") && actions.includes("delete")) return "replace";
|
|
@@ -9169,51 +9347,51 @@ function actionVerb(action) {
|
|
|
9169
9347
|
}
|
|
9170
9348
|
function renderComment(estimate) {
|
|
9171
9349
|
const { coverage } = estimate;
|
|
9172
|
-
const
|
|
9173
|
-
|
|
9350
|
+
const lines2 = [COMMENT_MARKER, "### \u{1F4B2} Infrastructure cost estimate", ""];
|
|
9351
|
+
lines2.push(
|
|
9174
9352
|
`**Coverage: ${coverage.pricedResources} of ${coverage.pricedResources + coverage.unpricedResources} billable resources priced** (${coverage.freeResources} more have no cost by design).`
|
|
9175
9353
|
);
|
|
9176
|
-
|
|
9354
|
+
lines2.push("");
|
|
9177
9355
|
const display = estimate.displayCurrency?.approximate ? estimate.displayCurrency : null;
|
|
9178
9356
|
const delta = (usd, converted) => display && typeof converted === "number" ? fmtConverted(converted, display.code) : fmtUsd(usd);
|
|
9179
9357
|
if (estimate.totalMonthlyDeltaUsd !== null) {
|
|
9180
|
-
const
|
|
9181
|
-
|
|
9182
|
-
display ? `**Estimated change: ${
|
|
9358
|
+
const total2 = delta(estimate.totalMonthlyDeltaUsd, estimate.totalMonthlyDelta);
|
|
9359
|
+
lines2.push(
|
|
9360
|
+
display ? `**Estimated change: ${total2}** (list price, ${fmtUsd(estimate.totalMonthlyDeltaUsd)} in USD)` : `**Estimated change: ${total2}** (list price, USD)`
|
|
9183
9361
|
);
|
|
9184
9362
|
} else {
|
|
9185
|
-
|
|
9363
|
+
lines2.push(
|
|
9186
9364
|
`_Coverage is below the threshold needed to publish a total \u2014 see the breakdown below instead of a number that wouldn't hold up._`
|
|
9187
9365
|
);
|
|
9188
9366
|
}
|
|
9189
|
-
|
|
9190
|
-
|
|
9367
|
+
lines2.push("");
|
|
9368
|
+
lines2.push(
|
|
9191
9369
|
display ? `> These are **list prices**, not what you actually pay. Cloud providers publish them in USD; the amounts shown are **approximate**, converted to ${display.code} at the ECB reference rate${display.rateDate ? ` of ${display.rateDate}` : ""} (1 USD = ${display.rate} ${display.code}). ` + LIST_PRICE_CAVEAT : `> These are **list prices in USD**, not what you actually pay. Cloud providers set their list prices in USD, so showing this in another billing currency would be an estimate on top of an estimate. ${LIST_PRICE_CAVEAT}`
|
|
9192
9370
|
);
|
|
9193
9371
|
if (estimate.priced.length > 0) {
|
|
9194
|
-
|
|
9372
|
+
lines2.push("", "| Resource | Change | Qty | Delta |", "|---|---|---|---|");
|
|
9195
9373
|
for (const item of estimate.priced) {
|
|
9196
|
-
|
|
9374
|
+
lines2.push(
|
|
9197
9375
|
`| \`${item.type}\` | ${actionVerb(item.action)} | ${item.count} | ${delta(item.monthlyDeltaUsd, item.monthlyDelta)} |`
|
|
9198
9376
|
);
|
|
9199
9377
|
}
|
|
9200
9378
|
}
|
|
9201
9379
|
if (estimate.unpriced.length > 0) {
|
|
9202
|
-
|
|
9203
|
-
|
|
9380
|
+
lines2.push("", "<details><summary>Not priced (" + coverage.unpricedResources + " resources)</summary>", "");
|
|
9381
|
+
lines2.push("| Resource | Change | Qty | Why |", "|---|---|---|---|");
|
|
9204
9382
|
for (const item of estimate.unpriced) {
|
|
9205
|
-
|
|
9383
|
+
lines2.push(`| \`${item.type}\` | ${actionVerb(item.action)} | ${item.count} | ${reasonText(item.reason)} |`);
|
|
9206
9384
|
}
|
|
9207
|
-
|
|
9385
|
+
lines2.push("", "</details>");
|
|
9208
9386
|
}
|
|
9209
9387
|
const truncated = estimate.catalogs.filter((c) => c.truncated);
|
|
9210
9388
|
if (truncated.length > 0) {
|
|
9211
|
-
|
|
9389
|
+
lines2.push(
|
|
9212
9390
|
"",
|
|
9213
9391
|
`\u26A0\uFE0F The price catalog for ${truncated.map((c) => c.region).join(", ")} was truncated by pagination: some prices may be missing.`
|
|
9214
9392
|
);
|
|
9215
9393
|
}
|
|
9216
|
-
return
|
|
9394
|
+
return lines2.join("\n");
|
|
9217
9395
|
}
|
|
9218
9396
|
|
|
9219
9397
|
// src/security/drift.ts
|
|
@@ -9376,8 +9554,6 @@ function publicVulnerable(input, provider) {
|
|
|
9376
9554
|
}
|
|
9377
9555
|
|
|
9378
9556
|
// ../../services/security/src/rules/aws/combos.ts
|
|
9379
|
-
var DIA_MS = 24 * 60 * 60 * 1e3;
|
|
9380
|
-
var OLD_ACCESS_KEY_DAYS = 90;
|
|
9381
9557
|
function lambdaRef(resourceId, region) {
|
|
9382
9558
|
const partes = resourceId.split(":");
|
|
9383
9559
|
if (partes[0] === "arn" && partes[2] === "lambda" && partes[5] === "function" && partes[3] && partes[6]) {
|
|
@@ -9416,38 +9592,23 @@ var AWS_SECURITY_COMBOS = [
|
|
|
9416
9592
|
id: "COMBO_AWS_PUBLIC_DB_UNENCRYPTED",
|
|
9417
9593
|
provider: "aws",
|
|
9418
9594
|
severity: "critical",
|
|
9419
|
-
rules: ["SEC_AWS_ENCRYPTION_RDS_SIN_CIFRAR"],
|
|
9420
|
-
detect: (input) => input
|
|
9421
|
-
const db = observationOf(input, "rds_instance", f.region, f.resourceId);
|
|
9422
|
-
if (db?.attributes["publiclyAccessible"] !== true) return [];
|
|
9423
|
-
return [{ region: f.region, resourceId: f.resourceId, resourceName: f.resourceName, findingIds: [f.id], evidence: { publiclyAccessible: true } }];
|
|
9424
|
-
})
|
|
9595
|
+
rules: ["SEC_AWS_ENCRYPTION_RDS_SIN_CIFRAR", "SEC_AWS_EXPOSURE_RDS_PUBLICA"],
|
|
9596
|
+
detect: (input) => sameResource(input, "SEC_AWS_ENCRYPTION_RDS_SIN_CIFRAR", "SEC_AWS_EXPOSURE_RDS_PUBLICA").map((m) => ({ ...m, evidence: { publiclyAccessible: true } }))
|
|
9425
9597
|
},
|
|
9426
9598
|
{
|
|
9427
9599
|
// Entra a la consola solo con contraseña y además tiene una clave de acceso vieja y activa: dos puertas sin segundo factor.
|
|
9428
9600
|
id: "COMBO_AWS_USER_NO_MFA_OLD_KEY",
|
|
9429
9601
|
provider: "aws",
|
|
9430
9602
|
severity: "critical",
|
|
9431
|
-
rules: ["SEC_AWS_IAM_USUARIO_SIN_MFA"],
|
|
9432
|
-
detect: (input) =>
|
|
9433
|
-
const
|
|
9434
|
-
|
|
9435
|
-
|
|
9436
|
-
const
|
|
9437
|
-
return
|
|
9438
|
-
})
|
|
9439
|
-
|
|
9440
|
-
return [
|
|
9441
|
-
{
|
|
9442
|
-
region: f.region,
|
|
9443
|
-
resourceId: f.resourceId,
|
|
9444
|
-
resourceName: f.resourceName,
|
|
9445
|
-
findingIds: [f.id],
|
|
9446
|
-
// 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.
|
|
9447
|
-
evidence: { oldActiveKeys: viejas.length, oldestKeyCreatedAt: viejas[0] }
|
|
9448
|
-
}
|
|
9449
|
-
];
|
|
9450
|
-
})
|
|
9603
|
+
rules: ["SEC_AWS_IAM_USUARIO_SIN_MFA", "SEC_AWS_IAM_CLAVE_ANTIGUA"],
|
|
9604
|
+
detect: (input) => {
|
|
9605
|
+
const viejas = findingsByResource(input.findings, "SEC_AWS_IAM_CLAVE_ANTIGUA");
|
|
9606
|
+
return sameResource(input, "SEC_AWS_IAM_USUARIO_SIN_MFA", "SEC_AWS_IAM_CLAVE_ANTIGUA").map((m) => {
|
|
9607
|
+
const claves = viejas.get(`${m.region}::${m.resourceId}`)?.evidence["oldActiveKeys"];
|
|
9608
|
+
const lista = Array.isArray(claves) ? claves : [];
|
|
9609
|
+
return { ...m, evidence: { oldActiveKeys: lista.length, oldestKeyCreatedAt: lista[0]?.["createdAt"] ?? null } };
|
|
9610
|
+
});
|
|
9611
|
+
}
|
|
9451
9612
|
},
|
|
9452
9613
|
{
|
|
9453
9614
|
// Alcanzable desde internet y con un CVE crítico o alto que ya tiene exploit público: el ataque está escrito.
|
|
@@ -9926,6 +10087,31 @@ var rdsClusterSnapshotPublico = {
|
|
|
9926
10087
|
],
|
|
9927
10088
|
evaluate: (observation) => snapshotPublicoEvaluate(observation, RDS_CHECKS.clusterSnapshotAttributes)
|
|
9928
10089
|
};
|
|
10090
|
+
function rdsPublicaEvaluate(observation) {
|
|
10091
|
+
const publica = observation.attributes.publiclyAccessible;
|
|
10092
|
+
if (typeof publica !== "boolean") {
|
|
10093
|
+
return { status: "unknown", check: RDS_CHECKS.instances, attribute: "publiclyAccessible" };
|
|
10094
|
+
}
|
|
10095
|
+
if (!publica) return { status: "ok" };
|
|
10096
|
+
return {
|
|
10097
|
+
status: "finding",
|
|
10098
|
+
evidence: {
|
|
10099
|
+
publiclyAccessible: true,
|
|
10100
|
+
engine: observation.attributes.engine ?? null,
|
|
10101
|
+
securityGroupIds: observation.attributes.securityGroupIds ?? null
|
|
10102
|
+
}
|
|
10103
|
+
};
|
|
10104
|
+
}
|
|
10105
|
+
var rdsPublica = {
|
|
10106
|
+
id: "SEC_AWS_EXPOSURE_RDS_PUBLICA",
|
|
10107
|
+
scope: "resource",
|
|
10108
|
+
category: "security_exposure",
|
|
10109
|
+
severity: "high",
|
|
10110
|
+
provider: "aws",
|
|
10111
|
+
kinds: ["rds_instance"],
|
|
10112
|
+
requires: [REGION_CHECKS.listRegions, RDS_CHECKS.instances],
|
|
10113
|
+
evaluate: rdsPublicaEvaluate
|
|
10114
|
+
};
|
|
9929
10115
|
|
|
9930
10116
|
// ../../services/security/src/rules/aws/iam.ts
|
|
9931
10117
|
function falta4(check, attribute) {
|
|
@@ -9979,6 +10165,74 @@ var usuarioSinMfa = {
|
|
|
9979
10165
|
requires: [IAM_CHECKS.listUsers, IAM_CHECKS.loginProfile, IAM_CHECKS.mfaDevices],
|
|
9980
10166
|
evaluate: usuarioSinMfaEvaluate
|
|
9981
10167
|
};
|
|
10168
|
+
var ACCESS_KEY_MAX_AGE_DAYS = 90;
|
|
10169
|
+
var DIA_MS = 864e5;
|
|
10170
|
+
function claveAntiguaEvaluate(observation, ahora = Date.now()) {
|
|
10171
|
+
const claves = observation.attributes.accessKeys;
|
|
10172
|
+
if (!Array.isArray(claves)) return falta4(IAM_CHECKS.accessKeys, "accessKeys");
|
|
10173
|
+
const viejas = [];
|
|
10174
|
+
let indecisa = false;
|
|
10175
|
+
for (const entrada of claves) {
|
|
10176
|
+
if (typeof entrada !== "object" || entrada === null) {
|
|
10177
|
+
indecisa = true;
|
|
10178
|
+
continue;
|
|
10179
|
+
}
|
|
10180
|
+
const clave = entrada;
|
|
10181
|
+
if (clave.active === false) continue;
|
|
10182
|
+
const creada = typeof clave.createdAt === "string" ? Date.parse(clave.createdAt) : Number.NaN;
|
|
10183
|
+
if (clave.active !== true || Number.isNaN(creada)) {
|
|
10184
|
+
indecisa = true;
|
|
10185
|
+
continue;
|
|
10186
|
+
}
|
|
10187
|
+
if (ahora - creada > ACCESS_KEY_MAX_AGE_DAYS * DIA_MS) viejas.push({ id: clave.id ?? null, createdAt: clave.createdAt });
|
|
10188
|
+
}
|
|
10189
|
+
if (viejas.length === 0) return indecisa ? falta4(IAM_CHECKS.accessKeys, "accessKeys") : { status: "ok" };
|
|
10190
|
+
viejas.sort((a, b) => a.createdAt.localeCompare(b.createdAt));
|
|
10191
|
+
return { status: "finding", evidence: { oldActiveKeys: viejas, maxAgeDays: ACCESS_KEY_MAX_AGE_DAYS } };
|
|
10192
|
+
}
|
|
10193
|
+
var claveAntigua = {
|
|
10194
|
+
id: "SEC_AWS_IAM_CLAVE_ANTIGUA",
|
|
10195
|
+
scope: "resource",
|
|
10196
|
+
category: "security_iam",
|
|
10197
|
+
severity: "medium",
|
|
10198
|
+
provider: "aws",
|
|
10199
|
+
kinds: ["iam_user"],
|
|
10200
|
+
requires: [IAM_CHECKS.listUsers, IAM_CHECKS.accessKeys],
|
|
10201
|
+
evaluate: (observation) => claveAntiguaEvaluate(observation)
|
|
10202
|
+
};
|
|
10203
|
+
var PASSWORD_MIN_LENGTH = 14;
|
|
10204
|
+
var PASSWORD_REUSE_PREVENTION = 24;
|
|
10205
|
+
function politicaContrasenasEvaluate(observation) {
|
|
10206
|
+
const usuarios = observation.attributes.userCount;
|
|
10207
|
+
if (typeof usuarios !== "number") return falta4(IAM_CHECKS.accountSummary, "userCount");
|
|
10208
|
+
if (usuarios === 0) return { status: "ok" };
|
|
10209
|
+
if (!("passwordPolicy" in observation.attributes)) return falta4(IAM_CHECKS.passwordPolicy, "passwordPolicy");
|
|
10210
|
+
const politica = observation.attributes.passwordPolicy;
|
|
10211
|
+
if (politica === null) {
|
|
10212
|
+
return { status: "finding", evidence: { passwordPolicy: null, userCount: usuarios } };
|
|
10213
|
+
}
|
|
10214
|
+
if (typeof politica !== "object" || Array.isArray(politica)) return falta4(IAM_CHECKS.passwordPolicy, "passwordPolicy");
|
|
10215
|
+
const p = politica;
|
|
10216
|
+
const longitud = typeof p.minimumLength === "number" ? p.minimumLength : null;
|
|
10217
|
+
const reuso = typeof p.reusePrevention === "number" ? p.reusePrevention : null;
|
|
10218
|
+
const corta = longitud === null || longitud < PASSWORD_MIN_LENGTH;
|
|
10219
|
+
const reutilizable = reuso === null || reuso < PASSWORD_REUSE_PREVENTION;
|
|
10220
|
+
if (!corta && !reutilizable) return { status: "ok" };
|
|
10221
|
+
return {
|
|
10222
|
+
status: "finding",
|
|
10223
|
+
evidence: { minimumLength: longitud, reusePrevention: reuso, userCount: usuarios }
|
|
10224
|
+
};
|
|
10225
|
+
}
|
|
10226
|
+
var politicaContrasenas = {
|
|
10227
|
+
id: "SEC_AWS_IAM_POLITICA_CONTRASENAS",
|
|
10228
|
+
scope: "resource",
|
|
10229
|
+
category: "security_iam",
|
|
10230
|
+
severity: "low",
|
|
10231
|
+
provider: "aws",
|
|
10232
|
+
kinds: ["iam_account"],
|
|
10233
|
+
requires: [IAM_CHECKS.accountSummary, IAM_CHECKS.passwordPolicy],
|
|
10234
|
+
evaluate: politicaContrasenasEvaluate
|
|
10235
|
+
};
|
|
9982
10236
|
function exclusion(observation) {
|
|
9983
10237
|
const value = observation.attributes.excluded;
|
|
9984
10238
|
if (value === null) return { known: true, excluded: false };
|
|
@@ -10397,6 +10651,7 @@ var trailGestionIncompleta = {
|
|
|
10397
10651
|
|
|
10398
10652
|
// ../../services/security/src/collectors/internet.ts
|
|
10399
10653
|
var ADMIN_PORTS2 = [22, 3389];
|
|
10654
|
+
var DATABASE_PORTS = [1433, 1521, 2483, 2484, 3306, 5432, 6379, 9200, 9300, 27017, 27018, 27019];
|
|
10400
10655
|
var ETIQUETAS_INTERNET = /* @__PURE__ */ new Set(["*", "internet", "any", "/0"]);
|
|
10401
10656
|
function longitudPrefijo(cidr, maximo) {
|
|
10402
10657
|
const partes = cidr.split("/");
|
|
@@ -10437,13 +10692,21 @@ function protocolName(protocol) {
|
|
|
10437
10692
|
const p = protocol.trim().toLowerCase();
|
|
10438
10693
|
return POR_NUMERO2[p] ?? p;
|
|
10439
10694
|
}
|
|
10440
|
-
function
|
|
10695
|
+
function portsReached(ports, protocol, portRanges) {
|
|
10441
10696
|
const p = protocolName(protocol);
|
|
10442
10697
|
const todos = p === "*" || p === "any" || p === "all";
|
|
10443
|
-
if (todos) return [...
|
|
10698
|
+
if (todos) return [...ports];
|
|
10444
10699
|
if (p !== "tcp" && p !== "udp") return [];
|
|
10445
|
-
if (portRanges.length === 0) return [...
|
|
10446
|
-
return
|
|
10700
|
+
if (portRanges.length === 0) return [...ports];
|
|
10701
|
+
return ports.filter((puerto) => portRanges.some((rango) => cubrePuerto(rango, puerto)));
|
|
10702
|
+
}
|
|
10703
|
+
function adminPortsReached(protocol, portRanges) {
|
|
10704
|
+
return portsReached(ADMIN_PORTS2, protocol, portRanges);
|
|
10705
|
+
}
|
|
10706
|
+
function databasePortsReached(protocol, portRanges) {
|
|
10707
|
+
if (protocolName(protocol) === "udp") return [];
|
|
10708
|
+
if (adminPortsReached(protocol, portRanges).length > 0) return [];
|
|
10709
|
+
return portsReached(DATABASE_PORTS, protocol, portRanges);
|
|
10447
10710
|
}
|
|
10448
10711
|
function cadenas2(value) {
|
|
10449
10712
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
|
|
@@ -10679,6 +10942,67 @@ var adminAbiertoPorPrefixList = {
|
|
|
10679
10942
|
requires: [REGION_CHECKS.listRegions, EC2_CHECKS.securityGroups, EC2_CHECKS.prefixListEntries],
|
|
10680
10943
|
evaluate: prefixListEvaluate
|
|
10681
10944
|
};
|
|
10945
|
+
function puertosDeBaseDeDatos(regla) {
|
|
10946
|
+
if (typeof regla.protocol !== "string") return void 0;
|
|
10947
|
+
const protocolo = protocolName(regla.protocol);
|
|
10948
|
+
if (protocolo !== "tcp") return [];
|
|
10949
|
+
const desde = regla.fromPort;
|
|
10950
|
+
const hasta = regla.toPort;
|
|
10951
|
+
if (typeof desde !== "number" || typeof hasta !== "number") return void 0;
|
|
10952
|
+
if (ADMIN_PORTS2.some((puerto) => desde <= puerto && puerto <= hasta)) return [];
|
|
10953
|
+
return DATABASE_PORTS.filter((puerto) => desde <= puerto && puerto <= hasta);
|
|
10954
|
+
}
|
|
10955
|
+
function bdAbiertaEvaluate(observation) {
|
|
10956
|
+
const ingress = observation.attributes.ingressRules;
|
|
10957
|
+
if (!Array.isArray(ingress)) {
|
|
10958
|
+
return { status: "unknown", check: EC2_CHECKS.securityGroups, attribute: "ingressRules" };
|
|
10959
|
+
}
|
|
10960
|
+
const aperturas = [];
|
|
10961
|
+
const puertos = /* @__PURE__ */ new Set();
|
|
10962
|
+
let indecisa = false;
|
|
10963
|
+
for (const entrada of ingress) {
|
|
10964
|
+
if (typeof entrada !== "object" || entrada === null) {
|
|
10965
|
+
indecisa = true;
|
|
10966
|
+
continue;
|
|
10967
|
+
}
|
|
10968
|
+
const regla = entrada;
|
|
10969
|
+
const origenes = [...cadenas2(regla.cidrs), ...cadenas2(regla.ipv6Cidrs)];
|
|
10970
|
+
const abierto = anySourceOpensToInternet(origenes);
|
|
10971
|
+
if (abierto === void 0) {
|
|
10972
|
+
indecisa = true;
|
|
10973
|
+
continue;
|
|
10974
|
+
}
|
|
10975
|
+
if (!abierto) continue;
|
|
10976
|
+
const alcanzados = puertosDeBaseDeDatos(regla);
|
|
10977
|
+
if (alcanzados === void 0) {
|
|
10978
|
+
indecisa = true;
|
|
10979
|
+
continue;
|
|
10980
|
+
}
|
|
10981
|
+
if (alcanzados.length === 0) continue;
|
|
10982
|
+
for (const puerto of alcanzados) puertos.add(puerto);
|
|
10983
|
+
aperturas.push({
|
|
10984
|
+
protocol: regla.protocol,
|
|
10985
|
+
fromPort: regla.fromPort ?? null,
|
|
10986
|
+
toPort: regla.toPort ?? null,
|
|
10987
|
+
cidr: origenes.find((cidr) => sourceOpensToInternet(cidr) === true) ?? null
|
|
10988
|
+
});
|
|
10989
|
+
}
|
|
10990
|
+
if (aperturas.length > 0) {
|
|
10991
|
+
return { status: "finding", evidence: { ports: [...puertos].sort((a, b) => a - b), openings: aperturas } };
|
|
10992
|
+
}
|
|
10993
|
+
if (indecisa) return { status: "unknown", check: EC2_CHECKS.securityGroups, attribute: "ingressRules" };
|
|
10994
|
+
return { status: "ok" };
|
|
10995
|
+
}
|
|
10996
|
+
var bdAbiertaAInternet = {
|
|
10997
|
+
id: "SEC_AWS_NETWORK_BD_ABIERTO",
|
|
10998
|
+
scope: "resource",
|
|
10999
|
+
category: "security_network",
|
|
11000
|
+
severity: "high",
|
|
11001
|
+
provider: "aws",
|
|
11002
|
+
kinds: ["security_group"],
|
|
11003
|
+
requires: [REGION_CHECKS.listRegions, EC2_CHECKS.securityGroups],
|
|
11004
|
+
evaluate: bdAbiertaEvaluate
|
|
11005
|
+
};
|
|
10682
11006
|
|
|
10683
11007
|
// ../../services/security/src/rules/aws/monitoring.ts
|
|
10684
11008
|
var evento = (...nombres) => nombres.map((n) => `$.eventName=${n}`);
|
|
@@ -11010,17 +11334,21 @@ var secretoEnVariableDeEntorno = {
|
|
|
11010
11334
|
var AWS_SECURITY_RULES = [
|
|
11011
11335
|
adminAbiertoAInternet,
|
|
11012
11336
|
adminAbiertoPorPrefixList,
|
|
11337
|
+
bdAbiertaAInternet,
|
|
11013
11338
|
lightsailAdminAbierto,
|
|
11014
11339
|
nlbAdminAbierto,
|
|
11015
11340
|
bucketPublico,
|
|
11016
11341
|
ebsSnapshotPublico,
|
|
11017
11342
|
rdsSnapshotPublico,
|
|
11018
11343
|
rdsClusterSnapshotPublico,
|
|
11344
|
+
rdsPublica,
|
|
11019
11345
|
lambdaPublica,
|
|
11020
11346
|
kmsSinRotacion,
|
|
11021
11347
|
rdsSinCifrar,
|
|
11022
11348
|
rootSinMfa,
|
|
11023
11349
|
usuarioSinMfa,
|
|
11350
|
+
claveAntigua,
|
|
11351
|
+
politicaContrasenas,
|
|
11024
11352
|
adminEfectivo,
|
|
11025
11353
|
identidadSinUso,
|
|
11026
11354
|
claveSinUso,
|
|
@@ -11073,7 +11401,18 @@ var AZURE_STORAGE_CHECKS = {
|
|
|
11073
11401
|
};
|
|
11074
11402
|
var AZURE_SQL_CHECKS = {
|
|
11075
11403
|
list: "azure:sql:list",
|
|
11076
|
-
firewallRules: "azure:sql:firewallRules"
|
|
11404
|
+
firewallRules: "azure:sql:firewallRules",
|
|
11405
|
+
databases: "azure:sql:databases",
|
|
11406
|
+
transparentDataEncryption: "azure:sql:transparentDataEncryption"
|
|
11407
|
+
};
|
|
11408
|
+
var AZURE_KEYVAULT_CHECKS = {
|
|
11409
|
+
vaults: "azure:keyvault:vaults"
|
|
11410
|
+
};
|
|
11411
|
+
var AZURE_NETWORK_CHECKS = {
|
|
11412
|
+
virtualNetworks: "azure:network:virtualNetworks",
|
|
11413
|
+
networkWatchers: "azure:network:networkWatchers",
|
|
11414
|
+
flowLogs: "azure:network:flowLogs",
|
|
11415
|
+
networkInterfaces: "azure:network:networkInterfaces"
|
|
11077
11416
|
};
|
|
11078
11417
|
var AZURE_IAM_CHECKS = {
|
|
11079
11418
|
roleAssignments: "azure:iam:roleAssignments",
|
|
@@ -11132,6 +11471,44 @@ var sqlTlsDebil = {
|
|
|
11132
11471
|
requires: [AZURE_SQL_CHECKS.list],
|
|
11133
11472
|
evaluate: sqlTlsEvaluate
|
|
11134
11473
|
};
|
|
11474
|
+
function keyVaultSinPurgaEvaluate(observation) {
|
|
11475
|
+
if (!("purgeProtection" in observation.attributes)) {
|
|
11476
|
+
return { status: "unknown", check: AZURE_KEYVAULT_CHECKS.vaults, attribute: "purgeProtection" };
|
|
11477
|
+
}
|
|
11478
|
+
if (observation.attributes.purgeProtection === true) return { status: "ok" };
|
|
11479
|
+
return {
|
|
11480
|
+
status: "finding",
|
|
11481
|
+
evidence: { purgeProtection: false, softDelete: observation.attributes.softDelete ?? null }
|
|
11482
|
+
};
|
|
11483
|
+
}
|
|
11484
|
+
var keyVaultSinPurga = {
|
|
11485
|
+
id: "SEC_AZURE_ENCRYPTION_KEYVAULT_SIN_PURGA",
|
|
11486
|
+
scope: "resource",
|
|
11487
|
+
category: "security_encryption",
|
|
11488
|
+
severity: "medium",
|
|
11489
|
+
provider: "azure",
|
|
11490
|
+
kinds: ["key_vault"],
|
|
11491
|
+
requires: [AZURE_KEYVAULT_CHECKS.vaults],
|
|
11492
|
+
evaluate: keyVaultSinPurgaEvaluate
|
|
11493
|
+
};
|
|
11494
|
+
function sqlSinTdeEvaluate(observation) {
|
|
11495
|
+
const estado = observation.attributes.tdeState;
|
|
11496
|
+
if (typeof estado !== "string") {
|
|
11497
|
+
return { status: "unknown", check: AZURE_SQL_CHECKS.transparentDataEncryption, attribute: "tdeState" };
|
|
11498
|
+
}
|
|
11499
|
+
if (estado.toLowerCase() === "enabled") return { status: "ok" };
|
|
11500
|
+
return { status: "finding", evidence: { tdeState: estado } };
|
|
11501
|
+
}
|
|
11502
|
+
var sqlSinTde = {
|
|
11503
|
+
id: "SEC_AZURE_ENCRYPTION_SQL_SIN_TDE",
|
|
11504
|
+
scope: "resource",
|
|
11505
|
+
category: "security_encryption",
|
|
11506
|
+
severity: "high",
|
|
11507
|
+
provider: "azure",
|
|
11508
|
+
kinds: ["sql_database"],
|
|
11509
|
+
requires: [AZURE_SQL_CHECKS.list, AZURE_SQL_CHECKS.databases, AZURE_SQL_CHECKS.transparentDataEncryption],
|
|
11510
|
+
evaluate: sqlSinTdeEvaluate
|
|
11511
|
+
};
|
|
11135
11512
|
|
|
11136
11513
|
// ../../services/security/src/rules/azure/exposure.ts
|
|
11137
11514
|
function blobPublicoEvaluate(observation) {
|
|
@@ -11286,9 +11663,35 @@ var sinDiagnosticSettings = {
|
|
|
11286
11663
|
requires: [AZURE_LOGGING_CHECKS.diagnosticSettings],
|
|
11287
11664
|
evaluate: evaluate5
|
|
11288
11665
|
};
|
|
11666
|
+
function vnetSinFlowLogsEvaluate(observation) {
|
|
11667
|
+
const vnet = observation.attributes.vnetFlowLog;
|
|
11668
|
+
if (typeof vnet !== "boolean") {
|
|
11669
|
+
return { status: "unknown", check: AZURE_NETWORK_CHECKS.flowLogs, attribute: "vnetFlowLog" };
|
|
11670
|
+
}
|
|
11671
|
+
if (vnet) return { status: "ok" };
|
|
11672
|
+
const subnets = observation.attributes.subnets;
|
|
11673
|
+
if (!Array.isArray(subnets)) {
|
|
11674
|
+
return { status: "unknown", check: AZURE_NETWORK_CHECKS.networkInterfaces, attribute: "subnets" };
|
|
11675
|
+
}
|
|
11676
|
+
const sinRegistro = subnets.filter((subnet) => subnet.covered !== true && subnet.platform !== true).map((subnet) => subnet.name);
|
|
11677
|
+
if (sinRegistro.length === 0) return { status: "ok" };
|
|
11678
|
+
return { status: "finding", evidence: { unloggedSubnets: sinRegistro, subnetCount: subnets.length } };
|
|
11679
|
+
}
|
|
11680
|
+
var vnetSinFlowLogs = {
|
|
11681
|
+
id: "SEC_AZURE_LOGGING_VNET_SIN_FLOW_LOGS",
|
|
11682
|
+
scope: "resource",
|
|
11683
|
+
category: "security_logging",
|
|
11684
|
+
severity: "medium",
|
|
11685
|
+
provider: "azure",
|
|
11686
|
+
kinds: ["virtual_network"],
|
|
11687
|
+
// Las interfaces no van aquí: solo cuentan para subredes sin log propio, y su falta ya sale como `unknown` de esa VNet.
|
|
11688
|
+
requires: [AZURE_NETWORK_CHECKS.virtualNetworks, AZURE_NETWORK_CHECKS.networkWatchers, AZURE_NETWORK_CHECKS.flowLogs],
|
|
11689
|
+
evaluate: vnetSinFlowLogsEvaluate
|
|
11690
|
+
};
|
|
11289
11691
|
|
|
11290
11692
|
// ../../services/security/src/rules/azure/network.ts
|
|
11291
|
-
|
|
11693
|
+
var evaluate6 = (observation) => nsgAbierto(observation, adminPortsReached);
|
|
11694
|
+
function nsgAbierto(observation, reached) {
|
|
11292
11695
|
const ingress = observation.attributes.ingressRules;
|
|
11293
11696
|
if (!Array.isArray(ingress)) {
|
|
11294
11697
|
return { status: "unknown", check: AZURE_NSG_CHECKS.list, attribute: "ingressRules" };
|
|
@@ -11310,7 +11713,7 @@ function evaluate6(observation) {
|
|
|
11310
11713
|
}
|
|
11311
11714
|
if (!abierto) continue;
|
|
11312
11715
|
const protocol = typeof regla.protocol === "string" ? regla.protocol : "";
|
|
11313
|
-
const alcanzados =
|
|
11716
|
+
const alcanzados = reached(protocol, cadenas2(regla.ports));
|
|
11314
11717
|
if (alcanzados.length === 0) continue;
|
|
11315
11718
|
for (const puerto of alcanzados) puertos.add(puerto);
|
|
11316
11719
|
openings.push({ protocol: regla.protocol, ports: alcanzados, sources: regla.sources });
|
|
@@ -11333,24 +11736,38 @@ var nsgAdminAbierto = {
|
|
|
11333
11736
|
requires: [AZURE_NSG_CHECKS.list],
|
|
11334
11737
|
evaluate: evaluate6
|
|
11335
11738
|
};
|
|
11739
|
+
var nsgBdAbierto = {
|
|
11740
|
+
id: "SEC_AZURE_NETWORK_BD_ABIERTO",
|
|
11741
|
+
scope: "resource",
|
|
11742
|
+
category: "security_network",
|
|
11743
|
+
severity: "high",
|
|
11744
|
+
provider: "azure",
|
|
11745
|
+
kinds: ["nsg"],
|
|
11746
|
+
requires: [AZURE_NSG_CHECKS.list],
|
|
11747
|
+
evaluate: (observation) => nsgAbierto(observation, databasePortsReached)
|
|
11748
|
+
};
|
|
11336
11749
|
|
|
11337
11750
|
// ../../services/security/src/rules/azure/index.ts
|
|
11338
11751
|
var AZURE_SECURITY_RULES = [
|
|
11339
11752
|
nsgAdminAbierto,
|
|
11753
|
+
nsgBdAbierto,
|
|
11340
11754
|
storageBlobPublico,
|
|
11341
11755
|
sqlPublico,
|
|
11342
11756
|
storageTlsDebil,
|
|
11343
11757
|
sqlTlsDebil,
|
|
11758
|
+
keyVaultSinPurga,
|
|
11759
|
+
sqlSinTde,
|
|
11344
11760
|
rolPrivilegiado,
|
|
11345
11761
|
asignacionHuerfana,
|
|
11346
|
-
sinDiagnosticSettings
|
|
11762
|
+
sinDiagnosticSettings,
|
|
11763
|
+
vnetSinFlowLogs
|
|
11347
11764
|
];
|
|
11348
11765
|
|
|
11349
11766
|
// ../../services/security/src/rules/gcp/combos.ts
|
|
11350
11767
|
var minus = (s) => typeof s === "string" ? s.toLowerCase() : "";
|
|
11351
11768
|
var GCP_SECURITY_COMBOS = [
|
|
11352
11769
|
{
|
|
11353
|
-
// Abierta a internet, sin
|
|
11770
|
+
// Abierta a internet, sin uso en 7 días y facturando: se paga por una puerta que nadie usa.
|
|
11354
11771
|
id: "COMBO_GCP_PUBLIC_SQL_IDLE",
|
|
11355
11772
|
provider: "gcp",
|
|
11356
11773
|
severity: "high",
|
|
@@ -11415,11 +11832,25 @@ var GCP_COMPUTE_CHECKS = {
|
|
|
11415
11832
|
effectiveFirewalls: "gcp:compute:networks.getEffectiveFirewalls",
|
|
11416
11833
|
regions: "gcp:compute:regions",
|
|
11417
11834
|
regionFirewallPolicies: "gcp:compute:regionNetworkFirewallPolicies",
|
|
11418
|
-
sharedVpcHost: "gcp:compute:sharedVpcHostFirewalls"
|
|
11835
|
+
sharedVpcHost: "gcp:compute:sharedVpcHostFirewalls",
|
|
11836
|
+
subnetworks: "gcp:compute:subnetworks"
|
|
11837
|
+
};
|
|
11838
|
+
var GCP_NETWORK_MANAGEMENT_CHECKS = {
|
|
11839
|
+
vpcFlowLogsConfigs: "gcp:networkmanagement:vpcFlowLogsConfigs"
|
|
11840
|
+
};
|
|
11841
|
+
var GCP_KMS_CHECKS = {
|
|
11842
|
+
locations: "gcp:kms:locations",
|
|
11843
|
+
keyRings: "gcp:kms:keyRings",
|
|
11844
|
+
cryptoKeys: "gcp:kms:cryptoKeys"
|
|
11419
11845
|
};
|
|
11420
11846
|
var GCP_SQL_CHECKS = {
|
|
11421
11847
|
instances: "gcp:sql:instances"
|
|
11422
11848
|
};
|
|
11849
|
+
var GCP_STORAGE_CHECKS = {
|
|
11850
|
+
buckets: "gcp:storage:buckets",
|
|
11851
|
+
// storage.buckets.getIamPolicy no está en roles/viewer ni en roles/storage.viewer: lo trae roles/iam.securityReviewer.
|
|
11852
|
+
iamPolicy: "gcp:storage:buckets.getIamPolicy"
|
|
11853
|
+
};
|
|
11423
11854
|
var GCP_FUNCTIONS_CHECKS = {
|
|
11424
11855
|
list: "gcp:functions:list"
|
|
11425
11856
|
};
|
|
@@ -11480,6 +11911,63 @@ var sqlSinCmek = {
|
|
|
11480
11911
|
requires: [GCP_SQL_CHECKS.instances],
|
|
11481
11912
|
evaluate: sqlSinCmekEvaluate
|
|
11482
11913
|
};
|
|
11914
|
+
var SOLO_CIFRADO = /* @__PURE__ */ new Set(["ENCRYPTED_ONLY", "TRUSTED_CLIENT_CERTIFICATE_REQUIRED"]);
|
|
11915
|
+
function sqlSinSslEvaluate(observation) {
|
|
11916
|
+
const modo = observation.attributes.sslMode;
|
|
11917
|
+
if (typeof modo === "string" && modo.length > 0) {
|
|
11918
|
+
if (SOLO_CIFRADO.has(modo)) return { status: "ok" };
|
|
11919
|
+
return { status: "finding", evidence: { sslMode: modo, ipv4Enabled: observation.attributes.ipv4Enabled ?? null } };
|
|
11920
|
+
}
|
|
11921
|
+
const requiere = observation.attributes.requireSsl;
|
|
11922
|
+
if (requiere === true) return { status: "ok" };
|
|
11923
|
+
if (!("sslMode" in observation.attributes)) {
|
|
11924
|
+
return { status: "unknown", check: GCP_SQL_CHECKS.instances, attribute: "sslMode" };
|
|
11925
|
+
}
|
|
11926
|
+
if (requiere !== false && requiere !== null) {
|
|
11927
|
+
return { status: "unknown", check: GCP_SQL_CHECKS.instances, attribute: "requireSsl" };
|
|
11928
|
+
}
|
|
11929
|
+
return { status: "finding", evidence: { sslMode: null, requireSsl: requiere, ipv4Enabled: observation.attributes.ipv4Enabled ?? null } };
|
|
11930
|
+
}
|
|
11931
|
+
var sqlSinSsl = {
|
|
11932
|
+
id: "SEC_GCP_ENCRYPTION_SQL_SIN_SSL",
|
|
11933
|
+
scope: "resource",
|
|
11934
|
+
category: "security_encryption",
|
|
11935
|
+
severity: "medium",
|
|
11936
|
+
provider: "gcp",
|
|
11937
|
+
kinds: ["cloud_sql_instance"],
|
|
11938
|
+
requires: [GCP_SQL_CHECKS.instances],
|
|
11939
|
+
evaluate: sqlSinSslEvaluate
|
|
11940
|
+
};
|
|
11941
|
+
var KMS_MAX_ROTATION_DAYS = 90;
|
|
11942
|
+
function segundos(periodo) {
|
|
11943
|
+
const m = /^(\d+(?:\.\d+)?)s$/.exec(periodo);
|
|
11944
|
+
return m ? Number(m[1]) : void 0;
|
|
11945
|
+
}
|
|
11946
|
+
function kmsSinRotacionEvaluate2(observation) {
|
|
11947
|
+
const a = observation.attributes;
|
|
11948
|
+
if (!("purpose" in a) || !("primaryState" in a) || !("rotationPeriod" in a)) {
|
|
11949
|
+
return { status: "unknown", check: GCP_KMS_CHECKS.cryptoKeys, attribute: "rotationPeriod" };
|
|
11950
|
+
}
|
|
11951
|
+
if (a.purpose !== "ENCRYPT_DECRYPT" || a.primaryState !== "ENABLED") return { status: "ok" };
|
|
11952
|
+
if (a.importOnly === true || typeof a.protectionLevel === "string" && a.protectionLevel.startsWith("EXTERNAL")) {
|
|
11953
|
+
return { status: "ok" };
|
|
11954
|
+
}
|
|
11955
|
+
if (a.rotationPeriod === null) return { status: "finding", evidence: { rotationPeriod: null } };
|
|
11956
|
+
const s = typeof a.rotationPeriod === "string" ? segundos(a.rotationPeriod) : void 0;
|
|
11957
|
+
if (s === void 0) return { status: "unknown", check: GCP_KMS_CHECKS.cryptoKeys, attribute: "rotationPeriod" };
|
|
11958
|
+
if (s <= KMS_MAX_ROTATION_DAYS * 86400) return { status: "ok" };
|
|
11959
|
+
return { status: "finding", evidence: { rotationPeriodDays: Math.round(s / 86400) } };
|
|
11960
|
+
}
|
|
11961
|
+
var kmsSinRotacion2 = {
|
|
11962
|
+
id: "SEC_GCP_ENCRYPTION_KMS_SIN_ROTACION",
|
|
11963
|
+
scope: "resource",
|
|
11964
|
+
category: "security_encryption",
|
|
11965
|
+
severity: "medium",
|
|
11966
|
+
provider: "gcp",
|
|
11967
|
+
kinds: ["kms_key"],
|
|
11968
|
+
requires: [GCP_KMS_CHECKS.locations, GCP_KMS_CHECKS.keyRings, GCP_KMS_CHECKS.cryptoKeys],
|
|
11969
|
+
evaluate: kmsSinRotacionEvaluate2
|
|
11970
|
+
};
|
|
11483
11971
|
|
|
11484
11972
|
// ../../services/security/src/rules/gcp/exposure.ts
|
|
11485
11973
|
function evaluate7(observation) {
|
|
@@ -11494,6 +11982,28 @@ function evaluate7(observation) {
|
|
|
11494
11982
|
evidence: { authorizedNetworks: nets }
|
|
11495
11983
|
};
|
|
11496
11984
|
}
|
|
11985
|
+
function gcsPublicoEvaluate(observation) {
|
|
11986
|
+
if (observation.attributes.publicAccessPrevention === "enforced") return { status: "ok" };
|
|
11987
|
+
const bindings = observation.attributes.publicBindings;
|
|
11988
|
+
if (!Array.isArray(bindings)) {
|
|
11989
|
+
return { status: "unknown", check: GCP_STORAGE_CHECKS.iamPolicy, attribute: "publicBindings" };
|
|
11990
|
+
}
|
|
11991
|
+
if (bindings.length === 0) return { status: "ok" };
|
|
11992
|
+
return {
|
|
11993
|
+
status: "finding",
|
|
11994
|
+
evidence: { publicBindings: bindings, publicAccessPrevention: observation.attributes.publicAccessPrevention ?? null }
|
|
11995
|
+
};
|
|
11996
|
+
}
|
|
11997
|
+
var gcsPublico = {
|
|
11998
|
+
id: "SEC_GCP_EXPOSURE_GCS_PUBLICO",
|
|
11999
|
+
scope: "resource",
|
|
12000
|
+
category: "security_exposure",
|
|
12001
|
+
severity: "critical",
|
|
12002
|
+
provider: "gcp",
|
|
12003
|
+
kinds: ["gcs_bucket"],
|
|
12004
|
+
requires: [GCP_STORAGE_CHECKS.buckets, GCP_STORAGE_CHECKS.iamPolicy],
|
|
12005
|
+
evaluate: gcsPublicoEvaluate
|
|
12006
|
+
};
|
|
11497
12007
|
var sqlPublico2 = {
|
|
11498
12008
|
id: "SEC_GCP_EXPOSURE_SQL_PUBLICO",
|
|
11499
12009
|
scope: "resource",
|
|
@@ -11693,9 +12203,42 @@ var sinExportSink = {
|
|
|
11693
12203
|
requires: [GCP_LOGGING_CHECKS.sinks],
|
|
11694
12204
|
evaluate: evaluate8
|
|
11695
12205
|
};
|
|
12206
|
+
function vpcSinFlowLogsEvaluate(observation) {
|
|
12207
|
+
const subnets = observation.attributes.subnets;
|
|
12208
|
+
if (!Array.isArray(subnets)) {
|
|
12209
|
+
return { status: "unknown", check: GCP_COMPUTE_CHECKS.subnetworks, attribute: "subnets" };
|
|
12210
|
+
}
|
|
12211
|
+
const lista = subnets;
|
|
12212
|
+
const sinRegistro = lista.filter((subnet) => subnet.covered === false);
|
|
12213
|
+
if (sinRegistro.length > 0) {
|
|
12214
|
+
return {
|
|
12215
|
+
status: "finding",
|
|
12216
|
+
evidence: { unloggedSubnets: sinRegistro.map((subnet) => `${String(subnet.region)}/${String(subnet.name)}`), subnetCount: lista.length }
|
|
12217
|
+
};
|
|
12218
|
+
}
|
|
12219
|
+
if (lista.some((subnet) => subnet.covered !== true)) {
|
|
12220
|
+
return { status: "unknown", check: GCP_NETWORK_MANAGEMENT_CHECKS.vpcFlowLogsConfigs, attribute: "subnets" };
|
|
12221
|
+
}
|
|
12222
|
+
if (observation.attributes.subnetsComplete !== true) {
|
|
12223
|
+
return { status: "unknown", check: GCP_COMPUTE_CHECKS.subnetworks, attribute: "subnets" };
|
|
12224
|
+
}
|
|
12225
|
+
return { status: "ok" };
|
|
12226
|
+
}
|
|
12227
|
+
var vpcSinFlowLogs = {
|
|
12228
|
+
id: "SEC_GCP_LOGGING_VPC_SIN_FLOW_LOGS",
|
|
12229
|
+
scope: "resource",
|
|
12230
|
+
category: "security_logging",
|
|
12231
|
+
severity: "medium",
|
|
12232
|
+
provider: "gcp",
|
|
12233
|
+
kinds: ["vpc_network"],
|
|
12234
|
+
// Las configs de Network Management no van aquí: una red con `logConfig` en cada subred ya está bien sin ellas.
|
|
12235
|
+
requires: [GCP_COMPUTE_CHECKS.subnetworks],
|
|
12236
|
+
evaluate: vpcSinFlowLogsEvaluate
|
|
12237
|
+
};
|
|
11696
12238
|
|
|
11697
12239
|
// ../../services/security/src/rules/gcp/network.ts
|
|
11698
|
-
|
|
12240
|
+
var evaluate9 = (observation) => firewallAbierto(observation, adminPortsReached);
|
|
12241
|
+
function firewallAbierto(observation, reached) {
|
|
11699
12242
|
const direction = observation.attributes.direction;
|
|
11700
12243
|
const disabled = observation.attributes.disabled;
|
|
11701
12244
|
const sources = observation.attributes.sourceRanges;
|
|
@@ -11714,7 +12257,7 @@ function evaluate9(observation) {
|
|
|
11714
12257
|
if (typeof entry !== "object" || entry === null) continue;
|
|
11715
12258
|
const rule = entry;
|
|
11716
12259
|
const protocol = typeof rule.protocol === "string" ? rule.protocol : "";
|
|
11717
|
-
for (const puerto of
|
|
12260
|
+
for (const puerto of reached(protocol, cadenas2(rule.ports))) puertos.add(puerto);
|
|
11718
12261
|
}
|
|
11719
12262
|
if (puertos.size === 0) return { status: "ok" };
|
|
11720
12263
|
return {
|
|
@@ -11874,6 +12417,16 @@ var firewallAdminAbierto = {
|
|
|
11874
12417
|
requires: [GCP_COMPUTE_CHECKS.firewalls],
|
|
11875
12418
|
evaluate: evaluate9
|
|
11876
12419
|
};
|
|
12420
|
+
var firewallBdAbierto = {
|
|
12421
|
+
id: "SEC_GCP_NETWORK_BD_ABIERTO",
|
|
12422
|
+
scope: "resource",
|
|
12423
|
+
category: "security_network",
|
|
12424
|
+
severity: "high",
|
|
12425
|
+
provider: "gcp",
|
|
12426
|
+
kinds: ["firewall"],
|
|
12427
|
+
requires: [GCP_COMPUTE_CHECKS.firewalls],
|
|
12428
|
+
evaluate: (observation) => firewallAbierto(observation, databasePortsReached)
|
|
12429
|
+
};
|
|
11877
12430
|
|
|
11878
12431
|
// ../../services/security/src/rules/gcp/secrets.ts
|
|
11879
12432
|
var SOSPECHOSAS2 = ["PASSWORD", "PASSWD", "SECRET", "TOKEN", "APIKEY", "API_KEY", "PRIVATE_KEY", "CREDENTIAL"];
|
|
@@ -11908,10 +12461,14 @@ var secretoEnFunction = {
|
|
|
11908
12461
|
// ../../services/security/src/rules/gcp/index.ts
|
|
11909
12462
|
var GCP_SECURITY_RULES = [
|
|
11910
12463
|
firewallAdminAbierto,
|
|
12464
|
+
firewallBdAbierto,
|
|
11911
12465
|
politicaAdminAbierta,
|
|
11912
12466
|
sqlPublico2,
|
|
12467
|
+
gcsPublico,
|
|
11913
12468
|
discoSinCmek,
|
|
11914
12469
|
sqlSinCmek,
|
|
12470
|
+
sqlSinSsl,
|
|
12471
|
+
kmsSinRotacion2,
|
|
11915
12472
|
secretoEnFunction,
|
|
11916
12473
|
rolPrimitivo,
|
|
11917
12474
|
claveSaUserManaged,
|
|
@@ -11919,7 +12476,8 @@ var GCP_SECURITY_RULES = [
|
|
|
11919
12476
|
claveSaSinUso,
|
|
11920
12477
|
permisosSinUso2,
|
|
11921
12478
|
auditIncompleto,
|
|
11922
|
-
sinExportSink
|
|
12479
|
+
sinExportSink,
|
|
12480
|
+
vpcSinFlowLogs
|
|
11923
12481
|
];
|
|
11924
12482
|
|
|
11925
12483
|
// src/security/evaluate.ts
|
|
@@ -11953,7 +12511,8 @@ var UNCOVERED_TYPES = /* @__PURE__ */ new Set([
|
|
|
11953
12511
|
"google_compute_region_network_firewall_policy_rule",
|
|
11954
12512
|
"google_logging_project_sink",
|
|
11955
12513
|
"google_project_iam_audit_config",
|
|
11956
|
-
//
|
|
12514
|
+
// SEC_GCP_EXPOSURE_GCS_PUBLICO juzga el bucket desplegado; el plan reparte su política en tres tipos que aún no se cruzan.
|
|
12515
|
+
"google_storage_bucket",
|
|
11957
12516
|
"google_storage_bucket_iam_member",
|
|
11958
12517
|
"google_storage_bucket_iam_binding",
|
|
11959
12518
|
"google_storage_bucket_iam_policy"
|
|
@@ -12030,14 +12589,14 @@ function evaluatePlan(plan) {
|
|
|
12030
12589
|
}))].sort();
|
|
12031
12590
|
const accountRules = ALL_RULES.filter((rule) => rule.scope === "account" && providers.includes(rule.provider)).map((rule) => rule.id);
|
|
12032
12591
|
const uncoveredTypes = [...new Set(index.resources.map((resource) => resource.type).filter((type) => UNCOVERED_TYPES.has(type)))].sort();
|
|
12033
|
-
const
|
|
12592
|
+
const bySeverity2 = (a, b) => SEVERITY_ORDER[a.severity] - SEVERITY_ORDER[b.severity] || a.address.localeCompare(b.address);
|
|
12034
12593
|
return {
|
|
12035
12594
|
report: {
|
|
12036
12595
|
providers,
|
|
12037
12596
|
resourcesEvaluated: new Set(observations.map((observation) => observation.address)).size,
|
|
12038
|
-
findings: planFindings.sort(
|
|
12039
|
-
combos: combos.sort(
|
|
12040
|
-
notEvaluable: notEvaluable.sort(
|
|
12597
|
+
findings: planFindings.sort(bySeverity2),
|
|
12598
|
+
combos: combos.sort(bySeverity2),
|
|
12599
|
+
notEvaluable: notEvaluable.sort(bySeverity2),
|
|
12041
12600
|
accountRules,
|
|
12042
12601
|
uncoveredTypes,
|
|
12043
12602
|
passed
|
|
@@ -12053,7 +12612,7 @@ function blockingCount(report, threshold) {
|
|
|
12053
12612
|
|
|
12054
12613
|
// src/security/render.ts
|
|
12055
12614
|
import { createHash as createHash2 } from "node:crypto";
|
|
12056
|
-
var
|
|
12615
|
+
var SEVERITIES3 = ["critical", "high", "medium", "low"];
|
|
12057
12616
|
var BADGE = { critical: "\u{1F534} critical", high: "\u{1F7E0} high", medium: "\u{1F7E1} medium", low: "\u26AA low" };
|
|
12058
12617
|
var GAP_TEXT = {
|
|
12059
12618
|
known_after_apply: "value only known after apply",
|
|
@@ -12085,7 +12644,7 @@ function evidenceSummary(finding) {
|
|
|
12085
12644
|
return parts.join("; ");
|
|
12086
12645
|
}
|
|
12087
12646
|
function counts(report) {
|
|
12088
|
-
return
|
|
12647
|
+
return SEVERITIES3.map((severity) => `${report.findings.filter((f) => f.severity === severity).length} ${severity}`).join(", ");
|
|
12089
12648
|
}
|
|
12090
12649
|
function displayAddress(address) {
|
|
12091
12650
|
return address.replace(/\["((?:[^"\\]|\\.)*)"\]/g, (_, key) => `[#${createHash2("sha256").update(key).digest("hex").slice(0, 8)}]`);
|
|
@@ -12104,18 +12663,18 @@ function groupedGaps(items) {
|
|
|
12104
12663
|
function renderSecurityMarkdown(report) {
|
|
12105
12664
|
const drift = report.drift?.items ?? [];
|
|
12106
12665
|
if (report.resourcesEvaluated === 0 && report.uncoveredTypes.length === 0 && drift.length === 0) return null;
|
|
12107
|
-
const
|
|
12666
|
+
const lines2 = ["### \u{1F512} Security", ""];
|
|
12108
12667
|
if (report.resourcesEvaluated === 0) {
|
|
12109
|
-
|
|
12668
|
+
lines2.push("_No resource in this plan is covered by the security rules yet._");
|
|
12110
12669
|
} else if (report.findings.length === 0) {
|
|
12111
|
-
|
|
12670
|
+
lines2.push(
|
|
12112
12671
|
`**No findings** on the ${report.resourcesEvaluated} resources checked` + (report.notEvaluable.length > 0 ? `, but ${groupedGaps(report.notEvaluable).length} checks could not be decided before apply (below).` : ".")
|
|
12113
12672
|
);
|
|
12114
12673
|
} else {
|
|
12115
|
-
|
|
12674
|
+
lines2.push(`**Findings: ${counts(report)}** on ${report.resourcesEvaluated} resources checked.`);
|
|
12116
12675
|
}
|
|
12117
12676
|
if (report.findings.length > 0) {
|
|
12118
|
-
|
|
12677
|
+
lines2.push("", "| Severity | Rule | Resource | Change | Detail |", "|---|---|---|---|---|");
|
|
12119
12678
|
const rows = /* @__PURE__ */ new Map();
|
|
12120
12679
|
for (const finding of report.findings) {
|
|
12121
12680
|
const key = `${finding.ruleId}#${finding.address}`;
|
|
@@ -12125,35 +12684,35 @@ function renderSecurityMarkdown(report) {
|
|
|
12125
12684
|
}
|
|
12126
12685
|
for (const { finding, count } of rows.values()) {
|
|
12127
12686
|
const detail = [evidenceSummary(finding), count > 1 ? `\xD7${count}` : ""].filter(Boolean).join(" ");
|
|
12128
|
-
|
|
12687
|
+
lines2.push(
|
|
12129
12688
|
`| ${BADGE[finding.severity]} | ${cell(ruleTitle(finding.ruleId))} | ${code(finding.address)} | ${ACTION[finding.action] ?? finding.action} | ${cell(detail) || "\u2014"} |`
|
|
12130
12689
|
);
|
|
12131
12690
|
}
|
|
12132
12691
|
}
|
|
12133
12692
|
if (report.combos.length > 0) {
|
|
12134
|
-
|
|
12135
|
-
|
|
12693
|
+
lines2.push("", "**Toxic combinations** \u2014 findings that are worse together, fix these first:", "");
|
|
12694
|
+
lines2.push("| Severity | Combination | Resource |", "|---|---|---|");
|
|
12136
12695
|
for (const combo of report.combos) {
|
|
12137
|
-
|
|
12696
|
+
lines2.push(`| ${BADGE[combo.severity]} | ${cell(comboTitle(combo.comboId))} | ${code(combo.address)} |`);
|
|
12138
12697
|
}
|
|
12139
12698
|
}
|
|
12140
12699
|
if (drift.length > 0) {
|
|
12141
12700
|
const when = report.drift?.scanAt ? ` of ${report.drift.scanAt.slice(0, 10)}` : "";
|
|
12142
|
-
|
|
12143
|
-
|
|
12701
|
+
lines2.push("", `**Last scan vs. this plan** (AAA Sentinel scan${when}):`, "");
|
|
12702
|
+
lines2.push("| Severity | Rule | Resource | What it means |", "|---|---|---|---|");
|
|
12144
12703
|
for (const item of drift) {
|
|
12145
|
-
|
|
12704
|
+
lines2.push(`| ${BADGE[item.severity]} | ${cell(ruleTitle(item.ruleId))} | ${code(item.address)} | ${DRIFT_TEXT[item.status]} |`);
|
|
12146
12705
|
}
|
|
12147
12706
|
}
|
|
12148
12707
|
if (report.drift?.truncated) {
|
|
12149
|
-
|
|
12708
|
+
lines2.push("", "_The live scan returned only its 500 most severe findings: drift on lower ones may be missing._");
|
|
12150
12709
|
}
|
|
12151
12710
|
const gaps = groupedGaps(report.notEvaluable);
|
|
12152
12711
|
if (gaps.length > 0) {
|
|
12153
|
-
|
|
12154
|
-
|
|
12155
|
-
for (const gap of gaps)
|
|
12156
|
-
|
|
12712
|
+
lines2.push("", `<details><summary>Not evaluable before apply (${gaps.length})</summary>`, "");
|
|
12713
|
+
lines2.push("| Rule | Resource | Why |", "|---|---|---|");
|
|
12714
|
+
for (const gap of gaps) lines2.push(`| ${cell(ruleTitle(gap.ruleId))} | ${code(gap.address)} | ${GAP_TEXT[gap.reason]} |`);
|
|
12715
|
+
lines2.push("", "</details>");
|
|
12157
12716
|
}
|
|
12158
12717
|
const notes = [];
|
|
12159
12718
|
if (report.accountRules.length > 0) {
|
|
@@ -12163,31 +12722,31 @@ function renderSecurityMarkdown(report) {
|
|
|
12163
12722
|
notes.push(`Present in this plan but not checked before deploy: ${report.uncoveredTypes.map(code).join(", ")}.`);
|
|
12164
12723
|
}
|
|
12165
12724
|
notes.push("Evaluated on your machine with the same rules as the AAA Sentinel posture scan; the plan is not sent anywhere for this.");
|
|
12166
|
-
|
|
12167
|
-
return
|
|
12725
|
+
lines2.push("", ...notes.map((note) => `> ${note}`));
|
|
12726
|
+
return lines2.join("\n");
|
|
12168
12727
|
}
|
|
12169
12728
|
function renderSecurityText(report) {
|
|
12170
|
-
const
|
|
12171
|
-
if (report.resourcesEvaluated === 0)
|
|
12172
|
-
else
|
|
12729
|
+
const lines2 = ["Security"];
|
|
12730
|
+
if (report.resourcesEvaluated === 0) lines2.push(" No resource in this plan is covered by the security rules yet.");
|
|
12731
|
+
else lines2.push(` ${report.resourcesEvaluated} resources checked: ${counts(report)}`);
|
|
12173
12732
|
for (const finding of report.findings) {
|
|
12174
12733
|
const detail = evidenceSummary(finding);
|
|
12175
|
-
|
|
12734
|
+
lines2.push(` [${finding.severity.toUpperCase()}] ${ruleTitle(finding.ruleId)} - ${displayAddress(finding.address)} (${ACTION[finding.action] ?? finding.action})${detail ? ` - ${detail.replace(/`/g, "")}` : ""}`);
|
|
12176
12735
|
}
|
|
12177
12736
|
for (const combo of report.combos) {
|
|
12178
|
-
|
|
12737
|
+
lines2.push(` [${combo.severity.toUpperCase()}] combination: ${comboTitle(combo.comboId)} - ${displayAddress(combo.address)}`);
|
|
12179
12738
|
}
|
|
12180
12739
|
for (const item of report.drift?.items ?? []) {
|
|
12181
|
-
|
|
12740
|
+
lines2.push(` [LAST SCAN] ${ruleTitle(item.ruleId)} - ${displayAddress(item.address)}: ${DRIFT_TEXT[item.status]}`);
|
|
12182
12741
|
}
|
|
12183
12742
|
const gaps = groupedGaps(report.notEvaluable);
|
|
12184
12743
|
if (gaps.length > 0) {
|
|
12185
|
-
|
|
12186
|
-
for (const gap of gaps)
|
|
12744
|
+
lines2.push(` Not evaluable before apply (${gaps.length}):`);
|
|
12745
|
+
for (const gap of gaps) lines2.push(` - ${ruleTitle(gap.ruleId)} - ${displayAddress(gap.address)}: ${GAP_TEXT[gap.reason]}`);
|
|
12187
12746
|
}
|
|
12188
|
-
if (report.accountRules.length > 0)
|
|
12189
|
-
if (report.uncoveredTypes.length > 0)
|
|
12190
|
-
return
|
|
12747
|
+
if (report.accountRules.length > 0) lines2.push(` ${report.accountRules.length} account-wide rules are only checked by the live scan.`);
|
|
12748
|
+
if (report.uncoveredTypes.length > 0) lines2.push(` Not checked before deploy: ${report.uncoveredTypes.join(", ")}`);
|
|
12749
|
+
return lines2.join("\n");
|
|
12191
12750
|
}
|
|
12192
12751
|
|
|
12193
12752
|
// src/run.ts
|
|
@@ -12230,10 +12789,10 @@ function decodePlan(bytes) {
|
|
|
12230
12789
|
var fmtUsd2 = (n) => `${n < 0 ? "-" : "+"}$${Math.abs(n).toFixed(2)}/mo`;
|
|
12231
12790
|
function costText(estimate) {
|
|
12232
12791
|
const { coverage } = estimate;
|
|
12233
|
-
const
|
|
12792
|
+
const total2 = estimate.totalMonthlyDeltaUsd === null ? "no total (coverage too low)" : `${fmtUsd2(estimate.totalMonthlyDeltaUsd)} (list price, USD)`;
|
|
12234
12793
|
return [
|
|
12235
12794
|
"Cost",
|
|
12236
|
-
` Estimated change: ${
|
|
12795
|
+
` Estimated change: ${total2}`,
|
|
12237
12796
|
` Coverage: ${coverage.pricedResources} of ${coverage.pricedResources + coverage.unpricedResources} billable resources priced`
|
|
12238
12797
|
].join("\n");
|
|
12239
12798
|
}
|
|
@@ -12331,8 +12890,15 @@ async function fix(argv) {
|
|
|
12331
12890
|
if (outcome.stdout) console.log(outcome.stdout);
|
|
12332
12891
|
process.exitCode = outcome.exitCode;
|
|
12333
12892
|
}
|
|
12893
|
+
async function posture(argv) {
|
|
12894
|
+
const outcome = await runPostureCli(argv, { env: process.env });
|
|
12895
|
+
for (const warning of outcome.warnings) console.warn(`iac-cost: ${warning}`);
|
|
12896
|
+
if (outcome.stdout) console.log(outcome.stdout);
|
|
12897
|
+
process.exitCode = outcome.exitCode;
|
|
12898
|
+
}
|
|
12334
12899
|
async function main() {
|
|
12335
12900
|
if (process.argv[2] === "fix") return fix(process.argv.slice(3));
|
|
12901
|
+
if (process.argv[2] === "posture") return posture(process.argv.slice(3));
|
|
12336
12902
|
const options = parseCliArgs(process.argv.slice(2), process.env);
|
|
12337
12903
|
if ("error" in options) {
|
|
12338
12904
|
console.error(`iac-cost: ${options.error}
|