Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 48 additions & 13 deletions lib/get-resources.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,18 @@ function searchAllIamPolicies(project) {
return paginate(`${ASSET_API}/projects/${project}:searchAllIamPolicies`, { pageSize: 500 }, 'results');
}

function listBucketIamPolicyAssets(project) {
return paginate(
`${ASSET_API}/projects/${project}/assets`,
{
contentType: 'IAM_POLICY',
assetTypes: ['storage.googleapis.com/Bucket'],
pageSize: 500,
},
'assets',
);
}

// projects/-/serviceAccounts/{email} — валідний шлях в IAM API, project
// беремо з листа `-`: не треба окремо передавати project поруч з email.
async function listUserManagedKeyIds(email) {
Expand Down Expand Up @@ -272,6 +284,32 @@ function assetResId(entry, project) {
return 'other/' + stripPrefix(res, '//');
}

export function replaceBucketIamPolicies(searchEntries, bucketAssets) {
return [
...searchEntries.filter((entry) => entry.assetType !== 'storage.googleapis.com/Bucket'),
...bucketAssets.map((asset) => ({
assetType: asset.assetType,
resource: asset.name,
policy: asset.iamPolicy,
})),
];
}

export function liveIamPolicyIds(iamPolicies, project, includeSystem = false) {
const liveIam = [];
for (const entry of iamPolicies) {
const rid = assetResId(entry, project);
for (const binding of (entry.policy && entry.policy.bindings) || []) {
for (const member of binding.members || []) {
if (includeSystem || !isSystem(IAM_SYSTEM, member)) {
liveIam.push(`${rid}/${binding.role}/${member}`);
}
}
}
}
return liveIam;
}

function kccArId(ref) {
const ext = ref.external || '';
const m = ext.match(/\/repositories\/([^/]+)$/);
Expand Down Expand Up @@ -659,19 +697,16 @@ export async function collectNamespace(namespace, { includeSystem = false } = {}
// --- IAMPolicyMember ---------------------------------------------------------
// search-all-iam-policies — усі біндинги проєкту одразу, на будь-якому
// типі ресурсу, не тільки на трьох раніше підтримуваних (Project/SA/AR).
const iamPolicies = await searchAllIamPolicies(project);
const liveIam = [];
for (const entry of iamPolicies) {
const rid = assetResId(entry, project);
for (const binding of (entry.policy && entry.policy.bindings) || []) {
for (const member of binding.members || []) {
if (includeSystem || !isSystem(IAM_SYSTEM, member)) {
liveIam.push(`${rid}/${binding.role}/${member}`);
}
}
}
}
push('IAMPolicyMember', liveIam, 'gcp');
// searchAllIamPolicies uses a separate eventually-consistent search index
// which can omit bucket policies that assets.list already exposes. Keep the
// broad search for other resource types, but replace its bucket slice with
// the complete IAM_POLICY asset snapshot.
const [searchedIamPolicies, bucketIamPolicyAssets] = await Promise.all([
searchAllIamPolicies(project),
listBucketIamPolicyAssets(project),
]);
const iamPolicies = replaceBucketIamPolicies(searchedIamPolicies, bucketIamPolicyAssets);
push('IAMPolicyMember', liveIamPolicyIds(iamPolicies, project, includeSystem), 'gcp');

const runServiceRefs = new Map(runServiceItems.map((i) => [i.metadata && i.metadata.name, kccScopedId(i)]));
push('IAMPolicyMember', await kccField(
Expand Down
92 changes: 92 additions & 0 deletions test/iam-policy-inventory.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import { liveIamPolicyIds, replaceBucketIamPolicies } from '../lib/get-resources.mjs';

const searchEntries = [
{
assetType: 'cloudresourcemanager.googleapis.com/Project',
resource: '//cloudresourcemanager.googleapis.com/projects/nitraai',
policy: {
bindings: [{
role: 'roles/viewer',
members: ['user:reader@example.com'],
}],
},
},
{
assetType: 'storage.googleapis.com/Bucket',
resource: '//storage.googleapis.com/indexed-bucket',
policy: {
bindings: [{
role: 'roles/storage.objectViewer',
members: ['user:indexed@example.com'],
}],
},
},
];

const bucketAssets = [
{
assetType: 'storage.googleapis.com/Bucket',
name: '//storage.googleapis.com/indexed-bucket',
iamPolicy: {
bindings: [{
role: 'roles/storage.objectViewer',
members: ['user:indexed@example.com'],
}],
},
},
{
assetType: 'storage.googleapis.com/Bucket',
name: '//storage.googleapis.com/missing-from-search',
iamPolicy: {
bindings: [
{
role: 'roles/storage.legacyBucketOwner',
members: ['projectOwner:nitraai'],
},
{
role: 'roles/storage.objectAdmin',
members: ['serviceAccount:writer@nitraai.iam.gserviceaccount.com'],
},
{
role: 'roles/storage.objectViewer',
members: ['allUsers'],
},
],
},
},
];

test('replaces the incomplete bucket search slice with IAM_POLICY assets', () => {
const entries = replaceBucketIamPolicies(searchEntries, bucketAssets);
const bucketEntries = entries.filter((entry) => entry.assetType === 'storage.googleapis.com/Bucket');

assert.deepEqual(bucketEntries.map((entry) => entry.resource), [
'//storage.googleapis.com/indexed-bucket',
'//storage.googleapis.com/missing-from-search',
]);
assert.equal(entries.filter((entry) => entry.assetType === 'cloudresourcemanager.googleapis.com/Project').length, 1);
});

test('covers bucket service-account and public bindings while filtering legacy ACL noise', () => {
const entries = replaceBucketIamPolicies(searchEntries, bucketAssets);
const ids = liveIamPolicyIds(entries, 'nitraai');

assert.ok(ids.includes(
'bucket/missing-from-search/roles/storage.objectAdmin/serviceAccount:writer@nitraai.iam.gserviceaccount.com',
));
assert.ok(ids.includes(
'bucket/missing-from-search/roles/storage.objectViewer/allUsers',
));
assert.ok(!ids.some((id) => id.includes('projectOwner:nitraai')));
});

test('keeps legacy bucket bindings when system resources are explicitly included', () => {
const entries = replaceBucketIamPolicies(searchEntries, bucketAssets);
const ids = liveIamPolicyIds(entries, 'nitraai', true);

assert.ok(ids.includes(
'bucket/missing-from-search/roles/storage.legacyBucketOwner/projectOwner:nitraai',
));
});
Loading