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
2 changes: 2 additions & 0 deletions .github/workflows/adversarial-fuzz.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ on:
- "internal/scan/**"
- "internal/config/**"
- "internal/adversarial/**"
- "internal/connectors/**"
- "internal/runtimefuzz/**"
- "docs/connectors.md"
- "testdata/adversarial/**"
- "testdata/fuzz/**"
- ".github/workflows/adversarial-fuzz.yml"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ The alpha includes ten explainable rules:
## Current limitations

- Detection is heuristic and should be reviewed by a human.
- The alpha scans local repositories and configuration files; GitHub, GitLab, Docker and Kubernetes connectors are not implemented yet.
- The alpha scans local repositories and configuration files. A read-only connector metadata layer now normalizes GitHub, GitLab, Docker and Kubernetes discovery payloads; network clients and credential resolution remain outside the local scan path.
- No runtime proxy, IAM remediation, hosted control plane, dashboard or compliance certification is included.
- JSON and SARIF are stable report formats for static and runtime findings. Runtime snapshots can be compared with `agentctl runtime-diff` and exported as CSV or HTML; issue export is planned.

Expand Down
7 changes: 7 additions & 0 deletions docs/cloud-audit-policy.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Cloud audit correlation and draft policies

The cloud audit package normalizes metadata-only events from AWS, GCP, and Azure into a common event shape. It accepts provider-exported JSON and extracts timestamp, principal, action, resource, environment, agent identity, and success state. It does not resolve credentials, call cloud APIs, read payloads, or retrieve secret values.

The policy package creates a `policy-draft.v1` object from runtime usage and normalized cloud resources. Drafts contain observed targets and operations, cloud resources correlated by stable agent ID, and an explicit `review_required: true` marker. Draft generation never blocks traffic, changes IAM, writes cloud policies, or enables enforcement automatically.

A future orchestration layer may load cloud events from CloudTrail, GCP Audit Logs, or Azure Activity Logs. That layer must make credentials and retention explicit, keep raw event payloads out of findings, and pass only normalized events into correlation.
18 changes: 18 additions & 0 deletions docs/connectors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Read-only connector metadata

The connector package defines a small normalization boundary for external discovery systems. It accepts already-fetched JSON metadata and returns a bounded `Metadata` object for inventory enrichment.

Supported kinds are GitHub repositories, GitLab projects, Docker image inspect metadata, and Kubernetes workload metadata. The package deliberately does not create network clients, resolve credentials, execute provider CLIs, or read Kubernetes Secret values. Those responsibilities belong to an explicit orchestration layer that can enforce user-selected credentials and audit access.

The normalized object contains only source locator, repository/project/image/workload identity, revision, namespace, labels, and annotations. Label and annotation keys or values containing secret, token, password, API key, authorization, or credential markers are discarded. Values are length-bounded and unsupported or malformed JSON is rejected.

A future connector implementation should satisfy:

```go
type Connector interface {
Kind() Kind
Normalize(payload []byte) (Metadata, error)
}
```

The local scan path remains deterministic and network-free. Connector discovery should be performed before scanning and passed into inventory as an explicit, reviewable input.
114 changes: 114 additions & 0 deletions internal/cloudaudit/audit.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package cloudaudit

import (
"encoding/json"
"fmt"
"strings"
"time"
)

type Provider string

const (
AWS Provider = "aws"
GCP Provider = "gcp"
Azure Provider = "azure"
)

type Event struct {
Timestamp time.Time `json:"timestamp"`
Provider Provider `json:"provider"`
AgentID string `json:"agent_id,omitempty"`
Principal string `json:"principal,omitempty"`
Action string `json:"action"`
Resource string `json:"resource"`
Environment string `json:"environment,omitempty"`
Success bool `json:"success"`
}

type Options struct {
MaxEvents int
MaxLineBytes int
}

func (o Options) withDefaults() Options {
if o.MaxEvents <= 0 {
o.MaxEvents = 1000000
}
if o.MaxLineBytes <= 0 {
o.MaxLineBytes = 1 << 20
}
return o
}

func Normalize(provider Provider, payload []byte) (Event, error) {
var value struct {
Timestamp string `json:"timestamp"`
EventTime string `json:"eventTime"`
AgentID string `json:"agent_id"`
Principal string `json:"principal"`
UserIdentity struct {
ARN string `json:"arn"`
Email string `json:"email"`
} `json:"userIdentity"`
Action string `json:"action"`
EventName string `json:"eventName"`
MethodName string `json:"methodName"`
Resource string `json:"resource"`
ResourceName string `json:"resourceName"`
Environment string `json:"environment"`
Success *bool `json:"success"`
ErrorCode string `json:"errorCode"`
}
if err := json.Unmarshal(payload, &value); err != nil {
return Event{}, fmt.Errorf("decode cloud audit event: %w", err)
}
if provider != AWS && provider != GCP && provider != Azure {
return Event{}, fmt.Errorf("unsupported cloud provider %q", provider)
}
timestamp := value.Timestamp
if timestamp == "" {
timestamp = value.EventTime
}
parsed, err := time.Parse(time.RFC3339, timestamp)
if err != nil {
return Event{}, fmt.Errorf("cloud audit timestamp: %w", err)
}
action := value.Action
if action == "" {
action = value.EventName
}
if action == "" {
action = value.MethodName
}
resource := value.Resource
if resource == "" {
resource = value.ResourceName
}
if strings.TrimSpace(action) == "" || strings.TrimSpace(resource) == "" {
return Event{}, fmt.Errorf("cloud audit action and resource are required")
}
principal := value.Principal
if principal == "" {
principal = value.UserIdentity.ARN
}
if principal == "" {
principal = value.UserIdentity.Email
}
success := true
if value.Success != nil {
success = *value.Success
}
if value.ErrorCode != "" {
success = false
}
return Event{Timestamp: parsed.UTC(), Provider: provider, AgentID: bounded(value.AgentID, 512), Principal: bounded(principal, 512), Action: bounded(action, 512), Resource: bounded(resource, 1024), Environment: bounded(value.Environment, 128), Success: success}, nil
}

func bounded(value string, max int) string {
value = strings.TrimSpace(value)
if len(value) > max {
return value[:max]
}
return value
}
24 changes: 24 additions & 0 deletions internal/cloudaudit/audit_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package cloudaudit

import (
"testing"
)

func TestNormalizeCloudAuditEvent(t *testing.T) {
event, err := Normalize(AWS, []byte(`{"eventTime":"2026-01-02T03:04:05Z","userIdentity":{"arn":"arn:aws:iam::123:role/support"},"eventName":"PutItem","resource":"arn:aws:dynamodb:us-east-1:123:table/customers","errorCode":""}`))
if err != nil {
t.Fatalf("Normalize() error = %v", err)
}
if event.Action != "PutItem" || event.Resource == "" || !event.Success {
t.Fatalf("unexpected event: %+v", event)
}
}

func TestNormalizeRejectsUnsupportedAndIncomplete(t *testing.T) {
if _, err := Normalize("oracle", []byte(`{}`)); err == nil {
t.Fatal("unsupported provider accepted")
}
if _, err := Normalize(GCP, []byte(`{"timestamp":"2026-01-02T03:04:05Z"}`)); err == nil {
t.Fatal("incomplete event accepted")
}
}
Loading
Loading