UNPKG

@cloud-copilot/iam-collect

Version:

Collect IAM information from AWS Accounts

461 lines 22.6 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.SqliteAwsIamStore = void 0; const iam_utils_1 = require("@cloud-copilot/iam-utils"); const better_sqlite3_1 = __importDefault(require("better-sqlite3")); const crypto_1 = require("crypto"); const json_js_1 = require("../../utils/json.js"); function quote(value) { if (value === undefined || value === null || value === '') { return 'NULL'; } return `'${String(value).replace(/'/g, "''")}'`; } const CURRENT_SCHEMA_VERSION = '2025-10-14'; /** * A SQLite-based implementation of the AwsIamStore interface. */ class SqliteAwsIamStore { dbPath; partition; iamCollectVersion; db; constructor(dbPath, partition, iamCollectVersion) { this.dbPath = dbPath; this.partition = partition; this.iamCollectVersion = iamCollectVersion; this.db = new better_sqlite3_1.default(this.dbPath); this.init(); } close() { this.db.close(); } async writeBatch(fn) { this.run('BEGIN'); try { await fn(); this.run('COMMIT'); } catch (error) { this.run('ROLLBACK'); throw error; } } /** * Returns the SQL DDL for a SQLite database. * * @returns The DDL to create the schema in a SQLite database. */ static schemaSql(iamCollectVersion) { return ` CREATE TABLE IF NOT EXISTS resource_metadata ( partition TEXT NOT NULL, account_id TEXT NOT NULL, arn TEXT NOT NULL, metadata_type TEXT NOT NULL, data TEXT NOT NULL, service TEXT NOT NULL, region TEXT, resource_type TEXT, arn_account TEXT, PRIMARY KEY (partition, account_id, arn, metadata_type) ); CREATE TABLE IF NOT EXISTS account_metadata ( partition TEXT NOT NULL, account_id TEXT NOT NULL, metadata_type TEXT NOT NULL, data TEXT NOT NULL, PRIMARY KEY (partition, account_id, metadata_type) ); CREATE TABLE IF NOT EXISTS organization_metadata ( partition TEXT NOT NULL, organization_id TEXT NOT NULL, metadata_type TEXT NOT NULL, data TEXT NOT NULL, PRIMARY KEY (partition, organization_id, metadata_type) ); CREATE TABLE IF NOT EXISTS organizational_unit_metadata ( partition TEXT NOT NULL, organization_id TEXT NOT NULL, ou_id TEXT NOT NULL, metadata_type TEXT NOT NULL, data TEXT NOT NULL, PRIMARY KEY (partition, organization_id, ou_id, metadata_type) ); CREATE TABLE IF NOT EXISTS organization_policy_metadata ( partition TEXT NOT NULL, organization_id TEXT NOT NULL, policy_type TEXT NOT NULL, policy_id TEXT NOT NULL, metadata_type TEXT NOT NULL, data TEXT NOT NULL, PRIMARY KEY (partition, organization_id, policy_type, policy_id, metadata_type) ); CREATE TABLE IF NOT EXISTS ram_resources ( partition TEXT NOT NULL, account_id TEXT NOT NULL, region TEXT NOT NULL, arn TEXT NOT NULL, data TEXT NOT NULL, PRIMARY KEY (partition, account_id, region, arn) ); CREATE TABLE IF NOT EXISTS indexes ( partition TEXT NOT NULL, index_name TEXT NOT NULL, data TEXT NOT NULL, hash TEXT NOT NULL, PRIMARY KEY (partition, index_name) ); CREATE TABLE IF NOT EXISTS metadata ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); INSERT OR IGNORE INTO metadata (key, value) VALUES ('schema_version', '${CURRENT_SCHEMA_VERSION}'); INSERT OR IGNORE INTO metadata (key, value) VALUES ('iam-collect_version', ${quote(iamCollectVersion)}); `; } init() { this.db.exec(SqliteAwsIamStore.schemaSql(this.iamCollectVersion)); } run(sql) { this.db.exec(sql); } query(sql) { return this.db.prepare(sql).all(); } isEmptyContent(content) { return (content === undefined || content === null || content === '' || content === '{}' || content === '[]' || (Array.isArray(content) && content.length === 0) || (typeof content === 'object' && Object.keys(content).length === 0)); } serialize(data) { return typeof data === 'string' ? data : (0, json_js_1.consistentStringify)(data); } async saveResourceMetadata(accountId, arn, metadataType, data) { accountId = accountId.toLowerCase(); arn = arn.toLowerCase(); metadataType = metadataType.toLowerCase(); if (this.isEmptyContent(data)) { await this.deleteResourceMetadata(accountId, arn, metadataType); return; } const parts = (0, iam_utils_1.splitArnParts)(arn); const service = (parts.service || '').toLowerCase(); const region = parts.region ? parts.region.toLowerCase() : null; const resourceType = parts.resourceType ? parts.resourceType.toLowerCase() : null; const content = this.serialize(data); const arnAccount = parts.accountId; const sql = `INSERT OR REPLACE INTO resource_metadata(partition, account_id, arn, metadata_type, data, service, region, resource_type, arn_account) VALUES(${quote(this.partition)}, ${quote(accountId)}, ${quote(arn)}, ${quote(metadataType)}, ${quote(content)}, ${quote(service)}, ${quote(region)}, ${quote(resourceType)}, ${quote(arnAccount)})`; this.run(sql); } async listResourceMetadata(accountId, arn) { accountId = accountId.toLowerCase(); arn = arn.toLowerCase(); const rows = this.query(`SELECT metadata_type FROM resource_metadata WHERE partition=${quote(this.partition)} AND account_id=${quote(accountId)} AND arn=${quote(arn)}`); return rows.map((r) => r.metadata_type); } async getResourceMetadata(accountId, arn, metadataType, defaultValue) { accountId = accountId.toLowerCase(); arn = arn.toLowerCase(); metadataType = metadataType.toLowerCase(); const rows = this.query(`SELECT data FROM resource_metadata WHERE partition=${quote(this.partition)} AND account_id=${quote(accountId)} AND arn=${quote(arn)} AND metadata_type=${quote(metadataType)} LIMIT 1`); if (rows.length === 0) { return defaultValue; } return JSON.parse(rows[0].data); } async deleteResourceMetadata(accountId, arn, metadataType) { accountId = accountId.toLowerCase(); arn = arn.toLowerCase(); metadataType = metadataType.toLowerCase(); const sql = `DELETE FROM resource_metadata WHERE partition=${quote(this.partition)} AND account_id=${quote(accountId)} AND arn=${quote(arn)} AND metadata_type=${quote(metadataType)}`; this.run(sql); } async deleteResource(accountId, arn) { accountId = accountId.toLowerCase(); arn = arn.toLowerCase(); const sql = `DELETE FROM resource_metadata WHERE partition=${quote(this.partition)} AND account_id=${quote(accountId)} AND arn=${quote(arn)}`; this.run(sql); } async listResources(accountId, options) { accountId = accountId.toLowerCase(); let sql = `SELECT DISTINCT arn FROM resource_metadata WHERE partition=${quote(this.partition)} AND account_id=${quote(accountId)} AND service=${quote(options.service.toLowerCase())}`; if (options.region) { sql += ` AND region=${quote(options.region.toLowerCase())}`; } else { sql += ` AND region IS NULL`; } if (options.resourceType) { sql += ` AND resource_type=${quote(options.resourceType.toLowerCase())}`; } else { sql += ` AND resource_type IS NULL`; } if (options.account) { sql += ` AND arn_account=${quote(options.account.toLowerCase())}`; } else { sql += ` AND arn_account IS NULL`; } const rows = this.query(sql); return rows.map((r) => r.arn); } async findResourceMetadata(accountId, options) { accountId = accountId.toLowerCase(); let sql = `SELECT data FROM resource_metadata WHERE partition=${quote(this.partition)} AND account_id=${quote(accountId)} AND service=${quote(options.service.toLowerCase())} AND metadata_type='metadata'`; if (options.region) { sql += ` AND region=${quote(options.region.toLowerCase())}`; } if (options.resourceType) { sql += ` AND resource_type=${quote(options.resourceType.toLowerCase())}`; } if (options.account) { sql += ` AND arn_account=${quote(options.account.toLowerCase())}`; } // Add JSON-based filtering for metadata if provided if (options.metadata) { for (const [key, value] of Object.entries(options.metadata)) { sql += ` AND json_extract(data, ${quote('$.' + key)}) = ${quote(value)}`; } } const rows = this.query(sql); const results = rows.map((r) => JSON.parse(r.data)); return results; } async syncResourceList(accountId, options, desiredResources) { accountId = accountId.toLowerCase(); let sql = `SELECT DISTINCT arn, data FROM resource_metadata WHERE partition=${quote(this.partition)} AND account_id=${quote(accountId)} AND service=${quote(options.service.toLowerCase())} AND metadata_type='metadata'`; if (options.region) { sql += ` AND region=${quote(options.region.toLowerCase())}`; } if (options.resourceType) { sql += ` AND resource_type=${quote(options.resourceType.toLowerCase())}`; } if (options.account) { sql += ` AND arn_account=${quote(options.account.toLowerCase())}`; } const rows = this.query(sql); let existing = rows.map((r) => ({ arn: r.arn, data: JSON.parse(r.data) })); if (options.metadata) { existing = existing.filter((item) => Object.entries(options.metadata).every(([k, v]) => item.data[k] === v)); } const keep = new Set(desiredResources.map((r) => r.toLowerCase())); for (const row of existing) { if (!keep.has(row.arn.toLowerCase())) { const delSql = `DELETE FROM resource_metadata WHERE partition=${quote(this.partition)} AND account_id=${quote(accountId)} AND arn=${quote(row.arn)}`; this.run(delSql); } } } async deleteAccountMetadata(accountId, metadataType) { accountId = accountId.toLowerCase(); metadataType = metadataType.toLowerCase(); const sql = `DELETE FROM account_metadata WHERE partition=${quote(this.partition)} AND account_id=${quote(accountId)} AND metadata_type=${quote(metadataType)}`; this.run(sql); } async saveAccountMetadata(accountId, metadataType, data) { accountId = accountId.toLowerCase(); metadataType = metadataType.toLowerCase(); if (this.isEmptyContent(data)) { await this.deleteAccountMetadata(accountId, metadataType); return; } const content = this.serialize(data); const sql = `INSERT OR REPLACE INTO account_metadata(partition, account_id, metadata_type, data) VALUES(${quote(this.partition)}, ${quote(accountId)}, ${quote(metadataType)}, ${quote(content)})`; this.run(sql); } async getAccountMetadata(accountId, metadataType, defaultValue) { accountId = accountId.toLowerCase(); metadataType = metadataType.toLowerCase(); const rows = this.query(`SELECT data FROM account_metadata WHERE partition=${quote(this.partition)} AND account_id=${quote(accountId)} AND metadata_type=${quote(metadataType)} LIMIT 1`); if (rows.length === 0) { return defaultValue; } return JSON.parse(rows[0].data); } async getOrganizationMetadata(organizationId, metadataType, defaultValue) { organizationId = organizationId.toLowerCase(); metadataType = metadataType.toLowerCase(); const rows = this.query(`SELECT data FROM organization_metadata WHERE partition=${quote(this.partition)} AND organization_id=${quote(organizationId)} AND metadata_type=${quote(metadataType)} LIMIT 1`); if (rows.length === 0) { return defaultValue; } return JSON.parse(rows[0].data); } async saveOrganizationMetadata(organizationId, metadataType, data) { organizationId = organizationId.toLowerCase(); metadataType = metadataType.toLowerCase(); if (this.isEmptyContent(data)) { await this.deleteOrganizationMetadata(organizationId, metadataType); return; } const content = this.serialize(data); const sql = `INSERT OR REPLACE INTO organization_metadata(partition, organization_id, metadata_type, data) VALUES(${quote(this.partition)}, ${quote(organizationId)}, ${quote(metadataType)}, ${quote(content)})`; this.run(sql); } async deleteOrganizationMetadata(organizationId, metadataType) { organizationId = organizationId.toLowerCase(); metadataType = metadataType.toLowerCase(); const sql = `DELETE FROM organization_metadata WHERE partition=${quote(this.partition)} AND organization_id=${quote(organizationId)} AND metadata_type=${quote(metadataType)}`; this.run(sql); } async listOrganizationalUnits(organizationId) { organizationId = organizationId.toLowerCase(); const rows = this.query(`SELECT DISTINCT ou_id FROM organizational_unit_metadata WHERE partition=${quote(this.partition)} AND organization_id=${quote(organizationId)}`); return rows.map((r) => r.ou_id); } async deleteOrganizationalUnitMetadata(organizationId, ouId, metadataType) { organizationId = organizationId.toLowerCase(); ouId = ouId.toLowerCase(); metadataType = metadataType.toLowerCase(); const sql = `DELETE FROM organizational_unit_metadata WHERE partition=${quote(this.partition)} AND organization_id=${quote(organizationId)} AND ou_id=${quote(ouId)} AND metadata_type=${quote(metadataType)}`; this.run(sql); } async saveOrganizationalUnitMetadata(organizationId, ouId, metadataType, data) { organizationId = organizationId.toLowerCase(); ouId = ouId.toLowerCase(); metadataType = metadataType.toLowerCase(); if (this.isEmptyContent(data)) { await this.deleteOrganizationalUnitMetadata(organizationId, ouId, metadataType); return; } const content = this.serialize(data); const sql = `INSERT OR REPLACE INTO organizational_unit_metadata(partition, organization_id, ou_id, metadata_type, data) VALUES(${quote(this.partition)}, ${quote(organizationId)}, ${quote(ouId)}, ${quote(metadataType)}, ${quote(content)})`; this.run(sql); } async getOrganizationalUnitMetadata(organizationId, ouId, metadataType, defaultValue) { organizationId = organizationId.toLowerCase(); ouId = ouId.toLowerCase(); metadataType = metadataType.toLowerCase(); const rows = this.query(`SELECT data FROM organizational_unit_metadata WHERE partition=${quote(this.partition)} AND organization_id=${quote(organizationId)} AND ou_id=${quote(ouId)} AND metadata_type=${quote(metadataType)} LIMIT 1`); if (rows.length === 0) { return defaultValue; } return JSON.parse(rows[0].data); } async deleteOrganizationalUnit(organizationId, ouId) { organizationId = organizationId.toLowerCase(); ouId = ouId.toLowerCase(); const sql = `DELETE FROM organizational_unit_metadata WHERE partition=${quote(this.partition)} AND organization_id=${quote(organizationId)} AND ou_id=${quote(ouId)}`; this.run(sql); } async deleteOrganizationPolicyMetadata(organizationId, policyType, policyId, metadataType) { organizationId = organizationId.toLowerCase(); policyType = policyType.toLowerCase(); policyId = policyId.toLowerCase(); metadataType = metadataType.toLowerCase(); const sql = `DELETE FROM organization_policy_metadata WHERE partition=${quote(this.partition)} AND organization_id=${quote(organizationId)} AND policy_type=${quote(policyType)} AND policy_id=${quote(policyId)} AND metadata_type=${quote(metadataType)}`; this.run(sql); } async saveOrganizationPolicyMetadata(organizationId, policyType, policyId, metadataType, data) { organizationId = organizationId.toLowerCase(); policyType = policyType.toLowerCase(); policyId = policyId.toLowerCase(); metadataType = metadataType.toLowerCase(); if (this.isEmptyContent(data)) { await this.deleteOrganizationPolicyMetadata(organizationId, policyType, policyId, metadataType); return; } const content = this.serialize(data); const sql = `INSERT OR REPLACE INTO organization_policy_metadata(partition, organization_id, policy_type, policy_id, metadata_type, data) VALUES(${quote(this.partition)}, ${quote(organizationId)}, ${quote(policyType)}, ${quote(policyId)}, ${quote(metadataType)}, ${quote(content)})`; this.run(sql); } async getOrganizationPolicyMetadata(organizationId, policyType, policyId, metadataType, defaultValue) { organizationId = organizationId.toLowerCase(); policyType = policyType.toLowerCase(); policyId = policyId.toLowerCase(); metadataType = metadataType.toLowerCase(); const rows = this.query(`SELECT data FROM organization_policy_metadata WHERE partition=${quote(this.partition)} AND organization_id=${quote(organizationId)} AND policy_type=${quote(policyType)} AND policy_id=${quote(policyId)} AND metadata_type=${quote(metadataType)} LIMIT 1`); if (rows.length === 0) { return defaultValue; } return JSON.parse(rows[0].data); } async deleteOrganizationPolicy(organizationId, policyType, policyId) { organizationId = organizationId.toLowerCase(); policyType = policyType.toLowerCase(); policyId = policyId.toLowerCase(); const sql = `DELETE FROM organization_policy_metadata WHERE partition=${quote(this.partition)} AND organization_id=${quote(organizationId)} AND policy_type=${quote(policyType)} AND policy_id=${quote(policyId)}`; this.run(sql); } async listOrganizationPolicies(organizationId, policyType) { organizationId = organizationId.toLowerCase(); policyType = policyType.toLowerCase(); const rows = this.query(`SELECT DISTINCT policy_id FROM organization_policy_metadata WHERE partition=${quote(this.partition)} AND organization_id=${quote(organizationId)} AND policy_type=${quote(policyType)}`); return rows.map((r) => r.policy_id); } async syncRamResources(accountId, region, arns) { accountId = accountId.toLowerCase(); const rg = region && region.trim() !== '' ? region.toLowerCase() : 'global'; const rows = this.query(`SELECT arn FROM ram_resources WHERE partition=${quote(this.partition)} AND account_id=${quote(accountId)} AND region=${quote(rg)}`); const keep = new Set(arns.map((a) => a.toLowerCase())); for (const row of rows) { if (!keep.has(row.arn.toLowerCase())) { const delSql = `DELETE FROM ram_resources WHERE partition=${quote(this.partition)} AND account_id=${quote(accountId)} AND region=${quote(rg)} AND arn=${quote(row.arn)}`; this.run(delSql); } } } async saveRamResource(accountId, arn, data) { accountId = accountId.toLowerCase(); arn = arn.toLowerCase(); const region = (0, iam_utils_1.splitArnParts)(arn).region; const rg = region && region.trim() !== '' ? region.toLowerCase() : 'global'; const content = this.serialize(data); const sql = `INSERT OR REPLACE INTO ram_resources(partition, account_id, region, arn, data) VALUES(${quote(this.partition)}, ${quote(accountId)}, ${quote(rg)}, ${quote(arn)}, ${quote(content)})`; this.run(sql); } async getRamResource(accountId, arn, defaultValue) { accountId = accountId.toLowerCase(); arn = arn.toLowerCase(); const region = (0, iam_utils_1.splitArnParts)(arn).region; const rg = region && region.trim() !== '' ? region.toLowerCase() : 'global'; const rows = this.query(`SELECT data FROM ram_resources WHERE partition=${quote(this.partition)} AND account_id=${quote(accountId)} AND region=${quote(rg)} AND arn=${quote(arn)} LIMIT 1`); if (rows.length === 0) { return defaultValue; } return JSON.parse(rows[0].data); } async listAccountIds() { const rows = this.query(`SELECT DISTINCT account_id FROM resource_metadata WHERE partition=${quote(this.partition)}`); return rows.map((r) => r.account_id); } async getIndex(indexName, defaultValue) { const rows = this.query(`SELECT data, hash FROM indexes WHERE partition=${quote(this.partition)} AND index_name=${quote(indexName.toLowerCase())} LIMIT 1`); if (rows.length === 0) { return { data: defaultValue, lockId: '' }; } return { data: JSON.parse(rows[0].data), lockId: rows[0].hash }; } async saveIndex(indexName, data, lockId) { const existing = this.query(`SELECT hash FROM indexes WHERE partition=${quote(this.partition)} AND index_name=${quote(indexName.toLowerCase())} LIMIT 1`); if (existing.length > 0 && existing[0].hash !== lockId) { return false; } if (existing.length === 0 && lockId !== '') { return false; } const content = (0, json_js_1.consistentStringify)(data); const hash = (0, crypto_1.createHash)('sha256').update(content).digest('hex'); const sql = `INSERT OR REPLACE INTO indexes(partition, index_name, data, hash) VALUES(${quote(this.partition)}, ${quote(indexName.toLowerCase())}, ${quote(content)}, ${quote(hash)})`; this.run(sql); return true; } } exports.SqliteAwsIamStore = SqliteAwsIamStore; //# sourceMappingURL=SqliteAwsIamStore.js.map