Files
heicode-mananger/heicode/controller/cloud_resource_discovery_test.go
T
chenchenandClaude Opus 4.8 df8fa80eba fix(discovery): address #5 review — no secret inheritance, GCP URL, tagged-only scope
回应 Fasthei 复审(PR #52 CHANGES_REQUESTED):
1. 发现出的子资源不再继承账号 secret_ref:抽出 DB 无关的 applyDiscoveredResourceFields,
   恒置 SecretRef=""(避免账号级凭据引用经 resourceToResponse / grant manifest 扩散到每个
   VM/S3/DB)。加 TestApplyDiscoveredResourceFields_NoSecretInheritance。
3. GCP searchAllResources URL 修正:gcpSearchAllResourcesURL 不再 PathEscape 整个 scope
   (slash 是路径模板一部分),生成 /v1/projects/<id>:searchAllResources。加 TestGCPSearchAllResourcesURL。
2. AWS 覆盖范围据实声明:Resource Groups Tagging GetResources 仅覆盖 tagged/曾 tagged 资源,
   不覆盖未打标签 EC2/RDS/S3;注释明确「tagged resources only」,#5 全量发现诉求不据此关闭。

controller 全套测试通过,go build/vet 干净。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-10 14:29:57 +08:00

151 lines
5.7 KiB
Go

package controller
import (
"crypto"
"crypto/rand"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"strings"
"testing"
"time"
"github.com/heicode/manager/model"
"github.com/stretchr/testify/require"
)
// #5: 跨云类别分类 vm/database/blob 一致。
func TestClassifyCloudResourceType(t *testing.T) {
cases := map[string]string{
// vm
"Microsoft.Compute/virtualMachines": "vm",
"AWS::EC2::Instance": "vm",
"compute.googleapis.com/Instance": "vm",
// database
"Microsoft.Sql/servers/databases": "database",
"AWS::RDS::DBInstance": "database",
"sqladmin.googleapis.com/Instance": "database",
"AWS::DynamoDB::Table": "database",
// blob / object storage
"Microsoft.Storage/storageAccounts": "blob",
"AWS::S3::Bucket": "blob",
"storage.googleapis.com/Bucket": "blob",
// fallback
"Microsoft.Network/virtualNetworks": "cloud_resource",
"AWS::IAM::Role": "cloud_resource",
}
for native, want := range cases {
require.Equal(t, want, classifyCloudResourceType(native), "classify %s", native)
}
}
// #5: ARN 解析 → 归一化 + 正确分类。
func TestAWSARNToResource(t *testing.T) {
ec2, ok := awsARNToResource("arn:aws:ec2:us-east-1:123456789012:instance/i-0abc123")
require.True(t, ok)
require.Equal(t, "us-east-1", ec2.Location)
require.Equal(t, "i-0abc123", ec2.Name)
require.Equal(t, "vm", classifyCloudResourceType(ec2.NativeType))
s3, ok := awsARNToResource("arn:aws:s3:::my-bucket")
require.True(t, ok)
require.Equal(t, "my-bucket", s3.Name)
require.Equal(t, "blob", classifyCloudResourceType(s3.NativeType))
rds, ok := awsARNToResource("arn:aws:rds:eu-west-1:123:db:prod-pg")
require.True(t, ok)
require.Equal(t, "database", classifyCloudResourceType(rds.NativeType))
_, ok = awsARNToResource("not-an-arn")
require.False(t, ok)
}
// #5: SigV4 实现正确性 —— 对齐 AWS 官方 "get-vanilla" 测试向量。
func TestAWSSigV4_VanillaVector(t *testing.T) {
cred := awsDiscoveryCredentials{
AccessKeyID: "AKIDEXAMPLE",
SecretAccessKey: "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY",
Region: "us-east-1",
}
tm := time.Date(2015, 8, 30, 12, 36, 0, 0, time.UTC)
auth, amzDate, err := awsSigV4Authorization("GET", "https://example.amazonaws.com/", map[string]string{}, []byte(""), cred, "service", tm)
require.NoError(t, err)
require.Equal(t, "20150830T123600Z", amzDate)
require.Contains(t, auth, "Credential=AKIDEXAMPLE/20150830/us-east-1/service/aws4_request")
require.Contains(t, auth, "SignedHeaders=host;x-amz-date")
// 官方向量期望签名
require.Contains(t, auth, "Signature=5fa00fa31553b73ebf1942676e86291e8372ff2a2260956d9b8aae1d763fbf31")
}
// #5 复审 #3:GCP searchAllResources URL —— scope 的 slash 不得被 %2F 转义。
func TestGCPSearchAllResourcesURL(t *testing.T) {
u := gcpSearchAllResourcesURL("my-proj", "")
require.Equal(t, "https://cloudasset.googleapis.com/v1/projects/my-proj:searchAllResources?pageSize=500", u)
require.NotContains(t, u, "%2F", "scope slash 不应被转义")
u2 := gcpSearchAllResourcesURL("my-proj", "tok en/+")
require.Contains(t, u2, "&pageToken=tok+en%2F%2B")
}
// #5 复审 #1:发现出的资源**绝不继承**账号 secret_ref;元数据带 classified_type,不含账号凭据引用。
func TestApplyDiscoveredResourceFields_NoSecretInheritance(t *testing.T) {
account := model.ResourceBinding{
Id: 42, UserId: 7, ResourceType: "cloud_account", Provider: "aws",
SecretRef: "azkv://heicode-kv.vault.azure.net/secrets/aws-keys",
}
d := discoveredCloudResource{
ExternalId: "arn:aws:ec2:us-east-1:123:instance/i-1",
Name: "i-1", NativeType: "AWS::EC2::Instance", Location: "us-east-1",
Metadata: map[string]any{"region": "us-east-1"},
}
var r model.ResourceBinding
require.NoError(t, applyDiscoveredResourceFields(&r, account, "aws", d))
require.Equal(t, "", r.SecretRef, "发现出的子资源绝不继承账号 secret_ref")
require.Equal(t, "cloud_resource", r.ResourceType)
require.Equal(t, "aws", r.Provider)
require.Equal(t, 7, r.UserId)
require.NotContains(t, r.Metadata, "azkv://", "metadata 不得含账号凭据引用")
require.Contains(t, r.Metadata, "\"classified_type\":\"vm\"")
require.Contains(t, r.PermissionScope, "aws:read")
}
// #5: GCP SA JWT —— RS256 断言可被对应公钥验签,且 claims 正确。
func TestBuildGCPAssertion_RS256Roundtrip(t *testing.T) {
key, err := rsa.GenerateKey(rand.Reader, 2048)
require.NoError(t, err)
der, err := x509.MarshalPKCS8PrivateKey(key)
require.NoError(t, err)
pemStr := string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: der}))
cred := gcpDiscoveryCredentials{
ClientEmail: "svc@proj.iam.gserviceaccount.com",
PrivateKey: pemStr,
ProjectID: "proj",
}
tm := time.Date(2026, 6, 10, 0, 0, 0, 0, time.UTC)
jwt, err := buildGCPAssertion(cred, "https://www.googleapis.com/auth/cloud-platform", "https://oauth2.googleapis.com/token", tm)
require.NoError(t, err)
parts := strings.Split(jwt, ".")
require.Len(t, parts, 3)
// 验签:signingInput = header.payload
signingInput := parts[0] + "." + parts[1]
digest := sha256.Sum256([]byte(signingInput))
sig, err := base64.RawURLEncoding.DecodeString(parts[2])
require.NoError(t, err)
require.NoError(t, rsa.VerifyPKCS1v15(&key.PublicKey, crypto.SHA256, digest[:], sig), "RS256 签名应可被公钥验证")
// header alg + claims
hdr, err := base64.RawURLEncoding.DecodeString(parts[0])
require.NoError(t, err)
require.Contains(t, string(hdr), "RS256")
claims, err := base64.RawURLEncoding.DecodeString(parts[1])
require.NoError(t, err)
require.Contains(t, string(claims), "svc@proj.iam.gserviceaccount.com")
require.Contains(t, string(claims), "cloud-platform")
}