@cloud-copilot/iam-collect
Version:
Collect IAM information from AWS Accounts
461 lines • 22.6 kB
JavaScript
;
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