diff --git a/docs/access-map-viewer/index.html b/docs/access-map-viewer/index.html index c4fb99a..1dfa1f6 100644 --- a/docs/access-map-viewer/index.html +++ b/docs/access-map-viewer/index.html @@ -1694,13 +1694,159 @@ container.appendChild(span); } + function setDetailName(text, link) { + const nameEl = document.getElementById("am-detail-name"); + nameEl.innerHTML = ""; + + if (link) { + const anchor = document.createElement("a"); + anchor.href = link; + anchor.target = "_blank"; + anchor.rel = "noopener noreferrer"; + anchor.textContent = text; + nameEl.appendChild(anchor); + } else { + nameEl.textContent = text; + } + } + + function extractResourceParts(label) { + if (!label) return { resourceType: "", resourceName: "" }; + const idx = label.indexOf(":"); + if (idx === -1) return { resourceType: "", resourceName: label }; + return { resourceType: label.slice(0, idx), resourceName: label.slice(idx + 1) }; + } + + function buildResourceConsoleLink(provider, resourceType, resourceName) { + if (!provider || !resourceName) return null; + const normalizedProvider = provider.toLowerCase(); + if (normalizedProvider === "aws") return awsResourceConsoleLink(resourceName); + if (normalizedProvider === "gcp") return gcpResourceConsoleLink(resourceName); + return null; + } + + function awsResourceConsoleLink(resource) { + if (!resource || !resource.startsWith("arn:")) return null; + const parts = resource.split(":"); + if (parts.length < 6) return null; + const service = parts[2]; + const region = parts[3]; + const resourcePart = parts[5] || ""; + + if (service === "s3") { + const bucket = resourcePart.replace(/^\/*/, ""); + return `https://console.aws.amazon.com/s3/buckets/${encodeURIComponent(bucket)}`; + } + + if (service === "iam") { + const match = resource.match(/^arn:aws:iam::\d+:([^/]+)\/(.+)$/); + const kind = match ? match[1] : null; + const res = match ? match[2] : null; + if (kind === "role") { + return `https://console.aws.amazon.com/iam/home?#/roles/${encodeURIComponent(res)}`; + } + if (kind === "user") { + return `https://console.aws.amazon.com/iam/home?#/users/${encodeURIComponent(res)}`; + } + return null; + } + + if (service === "lambda") { + const match = resourcePart.match(/^function[:\/](.+)$/); + if (match && match[1]) { + const fnName = match[1]; + const regionQuery = region ? `?region=${encodeURIComponent(region)}` : ""; + return `https://console.aws.amazon.com/lambda/home${regionQuery}#/functions/${encodeURIComponent(fnName)}`; + } + return null; + } + + if (service === "ec2") { + const match = resourcePart.match(/^instance\/(.+)$/); + if (match && match[1]) { + const instanceId = match[1]; + return `https://console.aws.amazon.com/ec2/v2/home?#InstanceDetails:instanceId=${encodeURIComponent(instanceId)}`; + } + return null; + } + + if (service === "kms") { + const match = resourcePart.match(/^(?:key|alias)\/(.+)$/); + if (match && match[1]) { + return `https://console.aws.amazon.com/kms/home?#/kms/keys/${encodeURIComponent(match[1])}`; + } + return null; + } + + if (service === "secretsmanager") { + return `https://console.aws.amazon.com/secretsmanager/home?#/secret?name=${encodeURIComponent(resource)}`; + } + + if (service === "dynamodb") { + const match = resourcePart.match(/^(?:table\/(.+)|table:(.+))/); + const tableRaw = match ? match[1] || match[2] : null; + const table = tableRaw ? tableRaw.split("/")[0] : null; + if (table) { + const regionQuery = region ? `?region=${encodeURIComponent(region)}` : ""; + return `https://console.aws.amazon.com/dynamodbv2/home${regionQuery}#/table/${encodeURIComponent(table)}/items`; + } + return null; + } + + return null; + } + + function gcpResourceConsoleLink(resource) { + if (!resource) return null; + const projectMatch = resource.match(/^projects\/([^/]+)/); + const project = projectMatch ? projectMatch[1] : null; + + if (resource.includes("/buckets/")) { + const bucketMatch = resource.match(/\/buckets\/([^/]+)/); + const bucket = bucketMatch ? bucketMatch[1] : null; + if (bucket && project) { + return `https://console.cloud.google.com/storage/browser/${encodeURIComponent(bucket)}?project=${encodeURIComponent(project)}`; + } + } + + if (resource.includes("/datasets/")) { + const datasetMatch = resource.match(/\/datasets\/([^/]+)/); + const dataset = datasetMatch ? datasetMatch[1] : null; + if (dataset && project) { + return `https://console.cloud.google.com/bigquery?project=${encodeURIComponent(project)}&p=${encodeURIComponent(project)}&d=${encodeURIComponent(dataset)}&page=dataset`; + } + } + + if (resource.includes("/secrets/")) { + const secretMatch = resource.match(/\/secrets\/([^/:]+)/); + const secret = secretMatch ? secretMatch[1] : null; + if (secret && project) { + return `https://console.cloud.google.com/security/secret-manager/secret/${encodeURIComponent(secret)}/versions?project=${encodeURIComponent(project)}`; + } + } + + if (resource.includes("/functions/")) { + const fnMatch = resource.match(/\/locations\/([^/]+)\/functions\/([^/]+)/); + if (fnMatch && project) { + const region = fnMatch[1]; + const fnName = fnMatch[2]; + return `https://console.cloud.google.com/functions/details/${encodeURIComponent(region)}/${encodeURIComponent(fnName)}?project=${encodeURIComponent(project)}`; + } + } + + if (project) { + return `https://console.cloud.google.com/home/dashboard?project=${encodeURIComponent(project)}`; + } + + return null; + } + function showAccessDetail(type, data) { document.getElementById("am-empty-state").classList.add("hidden"); const view = document.getElementById("am-detail-view"); view.classList.remove("hidden"); const icon = document.getElementById("am-detail-icon"); - const nameEl = document.getElementById("am-detail-name"); const meta = document.getElementById("am-detail-meta"); const typeField = document.getElementById("am-detail-type"); const cloudField = document.getElementById("am-detail-cloud"); @@ -1711,10 +1857,13 @@ permsList.innerHTML = ""; permsContainer.classList.add("hidden"); + let detailName = ""; + let detailLink = null; + if (type === "identity") { icon.textContent = "👤"; icon.className = "detail-icon-lg"; - nameEl.textContent = data.account || "(unknown identity)"; + detailName = data.account || "(unknown identity)"; typeField.textContent = "Identity"; const provider = (data.provider || "unknown").toUpperCase(); cloudField.textContent = provider; @@ -1724,7 +1873,10 @@ } else if (type === "resource") { icon.textContent = "📦"; icon.className = "detail-icon-lg"; - nameEl.textContent = data.name || "(resource)"; + const resourceLabel = data.name || "(resource)"; + const { resourceType, resourceName } = extractResourceParts(resourceLabel); + detailName = resourceLabel; + detailLink = buildResourceConsoleLink(data.provider, resourceType, resourceName); typeField.textContent = "Resource"; const provider = (data.provider || "unknown").toUpperCase(); cloudField.textContent = provider; @@ -1741,10 +1893,12 @@ } else if (type === "permission") { icon.textContent = "🔑"; icon.className = "detail-icon-lg"; - nameEl.textContent = data.name || "(permission)"; + detailName = data.name || "(permission)"; typeField.textContent = "Permission string"; cloudField.textContent = "-"; } + + setDetailName(detailName, detailLink); }