From c89588904c760df8f8945c71e3055b48ce9dfcd4 Mon Sep 17 00:00:00 2001 From: Manus AI Date: Sat, 12 Sep 2026 14:22:57 +0000 Subject: [PATCH 1/5] feat: add read-only connector metadata layer --- README.md | 2 +- docs/connectors.md | 18 ++ internal/connectors/connectors.go | 236 +++++++++++++++++++++++++ internal/connectors/connectors_test.go | 45 +++++ 4 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 docs/connectors.md create mode 100644 internal/connectors/connectors.go create mode 100644 internal/connectors/connectors_test.go diff --git a/README.md b/README.md index 25f6d13..b8d915f 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/docs/connectors.md b/docs/connectors.md new file mode 100644 index 0000000..d4ce4c0 --- /dev/null +++ b/docs/connectors.md @@ -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. diff --git a/internal/connectors/connectors.go b/internal/connectors/connectors.go new file mode 100644 index 0000000..115842c --- /dev/null +++ b/internal/connectors/connectors.go @@ -0,0 +1,236 @@ +package connectors + +import ( + "encoding/json" + "fmt" + "strings" +) + +type Kind string + +const ( + GitHub Kind = "github" + GitLab Kind = "gitlab" + Docker Kind = "docker" + Kubernetes Kind = "kubernetes" +) + +type Source struct { + Kind Kind `json:"kind"` + Name string `json:"name"` + Locator string `json:"locator"` +} +type Metadata struct { + Source Source `json:"source"` + Repository string `json:"repository,omitempty"` + Project string `json:"project,omitempty"` + Image string `json:"image,omitempty"` + Namespace string `json:"namespace,omitempty"` + Workload string `json:"workload,omitempty"` + Revision string `json:"revision,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Annotations map[string]string `json:"annotations,omitempty"` +} + +type Connector interface { + Kind() Kind + Normalize([]byte) (Metadata, error) +} + +type Registry struct{ connectors map[Kind]Connector } + +func NewRegistry() Registry { + return Registry{connectors: map[Kind]Connector{GitHub: githubConnector{}, GitLab: gitlabConnector{}, Docker: dockerConnector{}, Kubernetes: kubernetesConnector{}}} +} +func (r Registry) Normalize(kind Kind, payload []byte) (Metadata, error) { + connector, ok := r.connectors[kind] + if !ok { + return Metadata{}, fmt.Errorf("unsupported connector %q", kind) + } + return connector.Normalize(payload) +} + +func normalizeString(value string, max int) (string, error) { + value = strings.TrimSpace(value) + if len(value) > max { + return "", fmt.Errorf("metadata value exceeds %d bytes", max) + } + return value, nil +} +func normalizeMap(values map[string]string) map[string]string { + if len(values) == 0 { + return nil + } + result := make(map[string]string, len(values)) + for key, value := range values { + key = strings.TrimSpace(key) + value = strings.TrimSpace(value) + if key != "" && len(key) <= 256 && len(value) <= 1024 && !sensitiveKey(key) && !sensitiveKey(value) { + result[key] = value + } + } + if len(result) == 0 { + return nil + } + return result +} + +func sensitiveKey(value string) bool { + normalized := strings.ToLower(strings.NewReplacer("_", "", "-", "", ".", "").Replace(value)) + for _, part := range []string{"secret", "token", "password", "apikey", "authorization", "credential"} { + if strings.Contains(normalized, part) { + return true + } + } + return false +} + +func decode(data []byte, target any) error { + var raw map[string]json.RawMessage + if err := json.Unmarshal(data, &raw); err != nil { + return fmt.Errorf("decode connector metadata: %w", err) + } + if len(raw) == 0 { + return fmt.Errorf("connector metadata must be a JSON object") + } + if err := json.Unmarshal(data, target); err != nil { + return fmt.Errorf("decode connector metadata: %w", err) + } + return nil +} + +type githubConnector struct{} + +func (githubConnector) Kind() Kind { return GitHub } +func (githubConnector) Normalize(data []byte) (Metadata, error) { + var value struct { + FullName string `json:"full_name"` + Name string `json:"name"` + HTMLURL string `json:"html_url"` + DefaultBranch string `json:"default_branch"` + Topics []string `json:"topics"` + } + if err := decode(data, &value); err != nil { + return Metadata{}, err + } + repo, err := normalizeString(value.FullName, 512) + if err != nil { + return Metadata{}, err + } + if repo == "" { + repo, err = normalizeString(value.Name, 512) + if err != nil { + return Metadata{}, err + } + } + locator, err := normalizeString(value.HTMLURL, 1024) + if err != nil { + return Metadata{}, err + } + revision, err := normalizeString(value.DefaultBranch, 128) + if err != nil { + return Metadata{}, err + } + return Metadata{Source: Source{Kind: GitHub, Name: "GitHub repository", Locator: locator}, Repository: repo, Revision: revision, Labels: normalizeMap(mapFromList(value.Topics))}, nil +} + +type gitlabConnector struct{} + +func (gitlabConnector) Kind() Kind { return GitLab } +func (gitlabConnector) Normalize(data []byte) (Metadata, error) { + var value struct { + PathWithNamespace string `json:"path_with_namespace"` + Name string `json:"name"` + WebURL string `json:"web_url"` + DefaultBranch string `json:"default_branch"` + Topics []string `json:"topics"` + } + if err := decode(data, &value); err != nil { + return Metadata{}, err + } + project, err := normalizeString(value.PathWithNamespace, 512) + if err != nil { + return Metadata{}, err + } + if project == "" { + project, err = normalizeString(value.Name, 512) + if err != nil { + return Metadata{}, err + } + } + locator, err := normalizeString(value.WebURL, 1024) + if err != nil { + return Metadata{}, err + } + revision, err := normalizeString(value.DefaultBranch, 128) + if err != nil { + return Metadata{}, err + } + return Metadata{Source: Source{Kind: GitLab, Name: "GitLab project", Locator: locator}, Project: project, Revision: revision, Labels: normalizeMap(mapFromList(value.Topics))}, nil +} + +type dockerConnector struct{} + +func (dockerConnector) Kind() Kind { return Docker } +func (dockerConnector) Normalize(data []byte) (Metadata, error) { + var value struct { + RepoTags []string `json:"RepoTags"` + Config struct { + Labels map[string]string `json:"Labels"` + } `json:"Config"` + } + if err := decode(data, &value); err != nil { + return Metadata{}, err + } + image := "" + if len(value.RepoTags) > 0 { + image = value.RepoTags[0] + } + image, err := normalizeString(image, 1024) + if err != nil { + return Metadata{}, err + } + return Metadata{Source: Source{Kind: Docker, Name: "Docker image", Locator: image}, Image: image, Labels: normalizeMap(value.Config.Labels)}, nil +} + +type kubernetesConnector struct{} + +func (kubernetesConnector) Kind() Kind { return Kubernetes } +func (kubernetesConnector) Normalize(data []byte) (Metadata, error) { + var value struct { + Kind string `json:"kind"` + Metadata struct { + Name string `json:"name"` + Namespace string `json:"namespace"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` + } `json:"metadata"` + } + if err := decode(data, &value); err != nil { + return Metadata{}, err + } + workload, err := normalizeString(value.Metadata.Name, 512) + if err != nil { + return Metadata{}, err + } + namespace, err := normalizeString(value.Metadata.Namespace, 256) + if err != nil { + return Metadata{}, err + } + kind, err := normalizeString(value.Kind, 128) + if err != nil { + return Metadata{}, err + } + return Metadata{Source: Source{Kind: Kubernetes, Name: kind + " workload", Locator: namespace + "/" + workload}, Namespace: namespace, Workload: workload, Labels: normalizeMap(value.Metadata.Labels), Annotations: normalizeMap(value.Metadata.Annotations)}, nil +} + +func mapFromList(values []string) map[string]string { + result := make(map[string]string, len(values)) + for _, value := range values { + value = strings.TrimSpace(value) + if value != "" { + result[value] = "true" + } + } + return result +} diff --git a/internal/connectors/connectors_test.go b/internal/connectors/connectors_test.go new file mode 100644 index 0000000..3cdc74d --- /dev/null +++ b/internal/connectors/connectors_test.go @@ -0,0 +1,45 @@ +package connectors + +import ( + "encoding/json" + "strings" + "testing" +) + +func TestRegistryNormalizesMetadataOnly(t *testing.T) { + tests := []struct { + kind Kind + payload string + want string + }{ + {GitHub, `{"full_name":"HlinorAI/agent-control-plane","html_url":"https://github.com/HlinorAI/agent-control-plane","default_branch":"main","topics":["agents"]}`, "HlinorAI/agent-control-plane"}, + {GitLab, `{"path_with_namespace":"group/project","web_url":"https://gitlab.example/group/project","default_branch":"main"}`, "group/project"}, + {Docker, `{"RepoTags":["registry.example/agent:1.2.3"],"Config":{"Labels":{"owner":"security","secret":"must-not-be-read"}}}`, "registry.example/agent:1.2.3"}, + {Kubernetes, `{"kind":"Deployment","metadata":{"name":"support-agent","namespace":"production","labels":{"app":"support"},"annotations":{"owner":"security"}},"spec":{"env":[{"name":"TOKEN","value":"must-not-be-read"}]}}`, "production/support-agent"}, + } + registry := NewRegistry() + for _, test := range tests { + metadata, err := registry.Normalize(test.kind, []byte(test.payload)) + if err != nil { + t.Fatalf("Normalize(%s) error = %v", test.kind, err) + } + if metadata.Source.Locator != test.want && metadata.Repository != test.want && metadata.Project != test.want && metadata.Image != test.want { + t.Fatalf("Normalize(%s) locator mismatch: %+v", test.kind, metadata) + } + if strings.Contains(string(mustJSON(metadata)), "must-not-be-read") { + t.Fatalf("Normalize(%s) retained secret-like payload", test.kind) + } + } +} + +func TestRegistryRejectsUnsupportedAndMalformedInput(t *testing.T) { + registry := NewRegistry() + if _, err := registry.Normalize("aws", []byte(`{}`)); err == nil { + t.Fatal("unsupported connector accepted") + } + if _, err := registry.Normalize(GitHub, []byte(`[]`)); err == nil { + t.Fatal("array payload accepted") + } +} + +func mustJSON(value Metadata) []byte { payload, _ := json.Marshal(value); return payload } From 1afef1217df8bce7badd6348a1ead8c59ac8dc0d Mon Sep 17 00:00:00 2001 From: Manus AI Date: Sat, 12 Sep 2026 14:24:35 +0000 Subject: [PATCH 2/5] ci: run adversarial checks for connector changes --- .github/workflows/adversarial-fuzz.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/adversarial-fuzz.yml b/.github/workflows/adversarial-fuzz.yml index ce6e739..1300641 100644 --- a/.github/workflows/adversarial-fuzz.yml +++ b/.github/workflows/adversarial-fuzz.yml @@ -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" From c6724c3365700c5242d2cac96c6ea1f58701d140 Mon Sep 17 00:00:00 2001 From: Manus AI Date: Sat, 12 Sep 2026 14:26:59 +0000 Subject: [PATCH 3/5] feat: add cloud audit correlation and policy drafts --- docs/cloud-audit-policy.md | 7 ++ internal/cloudaudit/audit.go | 114 ++++++++++++++++++++++++++++++ internal/cloudaudit/audit_test.go | 24 +++++++ internal/policy/draft.go | 58 +++++++++++++++ internal/policy/draft_test.go | 19 +++++ 5 files changed, 222 insertions(+) create mode 100644 docs/cloud-audit-policy.md create mode 100644 internal/cloudaudit/audit.go create mode 100644 internal/cloudaudit/audit_test.go create mode 100644 internal/policy/draft.go create mode 100644 internal/policy/draft_test.go diff --git a/docs/cloud-audit-policy.md b/docs/cloud-audit-policy.md new file mode 100644 index 0000000..60efed6 --- /dev/null +++ b/docs/cloud-audit-policy.md @@ -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. diff --git a/internal/cloudaudit/audit.go b/internal/cloudaudit/audit.go new file mode 100644 index 0000000..96c841c --- /dev/null +++ b/internal/cloudaudit/audit.go @@ -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 +} diff --git a/internal/cloudaudit/audit_test.go b/internal/cloudaudit/audit_test.go new file mode 100644 index 0000000..0f8a908 --- /dev/null +++ b/internal/cloudaudit/audit_test.go @@ -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") + } +} diff --git a/internal/policy/draft.go b/internal/policy/draft.go new file mode 100644 index 0000000..27cd6aa --- /dev/null +++ b/internal/policy/draft.go @@ -0,0 +1,58 @@ +package policy + +import ( + "sort" + "strings" + + "github.com/HlinorAI/agent-control-plane/internal/cloudaudit" + "github.com/HlinorAI/agent-control-plane/internal/runtime" +) + +type Draft struct { + SchemaVersion string `json:"schema_version"` + AgentID string `json:"agent_id"` + Environment string `json:"environment,omitempty"` + AllowedTargets []string `json:"allowed_targets,omitempty"` + AllowedActions []string `json:"allowed_actions,omitempty"` + CloudResources []string `json:"cloud_resources,omitempty"` + ReviewRequired bool `json:"review_required"` +} + +func FromRuntime(agent runtime.AgentSummary, cloud []cloudaudit.Event) Draft { + draft := Draft{SchemaVersion: "policy-draft.v1", AgentID: agent.AgentID, ReviewRequired: true} + for _, environment := range agent.Environments { + if strings.EqualFold(environment, "production") || strings.EqualFold(environment, "prod") { + draft.Environment = "production" + break + } + if draft.Environment == "" { + draft.Environment = environment + } + } + draft.AllowedTargets = append([]string(nil), agent.Targets...) + for operation := range agent.Operations { + draft.AllowedActions = append(draft.AllowedActions, operation) + } + for _, event := range cloud { + if event.AgentID == agent.AgentID { + draft.CloudResources = appendUnique(draft.CloudResources, event.Resource) + } + } + sort.Strings(draft.AllowedTargets) + sort.Strings(draft.AllowedActions) + sort.Strings(draft.CloudResources) + return draft +} + +func appendUnique(values []string, value string) []string { + value = strings.TrimSpace(value) + if value == "" { + return values + } + for _, current := range values { + if current == value { + return values + } + } + return append(values, value) +} diff --git a/internal/policy/draft_test.go b/internal/policy/draft_test.go new file mode 100644 index 0000000..1d4e64c --- /dev/null +++ b/internal/policy/draft_test.go @@ -0,0 +1,19 @@ +package policy + +import ( + "testing" + "time" + + "github.com/HlinorAI/agent-control-plane/internal/cloudaudit" + "github.com/HlinorAI/agent-control-plane/internal/runtime" +) + +func TestFromRuntimeCreatesReviewableDraft(t *testing.T) { + draft := FromRuntime(runtime.AgentSummary{AgentID: "a1", Targets: []string{"crm.search"}, Environments: []string{"production"}, Operations: map[string]int{"tool_call": 3}}, []cloudaudit.Event{{AgentID: "a1", Timestamp: time.Now(), Action: "GetItem", Resource: "arn:aws:dynamodb:table/customers"}}) + if draft.SchemaVersion != "policy-draft.v1" || !draft.ReviewRequired || draft.Environment != "production" { + t.Fatalf("unexpected draft: %+v", draft) + } + if len(draft.CloudResources) != 1 || draft.CloudResources[0] == "" { + t.Fatalf("cloud resource missing: %+v", draft) + } +} From 37dc0c030ca3bf8e33b7a1b6529a225f7f2eee6f Mon Sep 17 00:00:00 2001 From: Manus AI Date: Sat, 12 Sep 2026 14:31:26 +0000 Subject: [PATCH 4/5] docs: update alpha release notes --- CHANGELOG.md | 30 ++++++++++++++++++++++++++++++ README.md | 31 ++++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..6bf503e --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ +# Changelog + +All notable changes to Agent Control Plane are documented here. + +## [0.1.0-alpha.3] - 2026-09-12 + +### Added + +- Runtime audit ingestion for JSONL, OpenTelemetry JSON, and API Gateway metadata. +- Streaming runtime aggregation with bounded input handling and agent-specific provider matching. +- Versioned `runtime.v1` event schema. +- Runtime findings with SARIF 2.1.0 export, JSON baseline support, and expiring suppressions. +- Rejection of sensitive runtime metadata fields, including prompts, arguments, bodies, headers, authorization data, tokens, and secrets. +- Deterministic `runtime-diff` snapshots with text, JSON, CSV, and HTML output. +- Read-only metadata normalizers for GitHub, GitLab, Docker, and Kubernetes. +- Metadata-only cloud audit normalization for AWS, GCP, and Azure. +- Review-only `policy-draft.v1` generation from runtime activity and correlated cloud resources. +- Adversarial and fuzz checks covering runtime parsers, metadata handling, and connector sanitization. + +### Security and boundaries + +This alpha remains a local, deterministic, read-only analysis tool. It does not execute scanned content, resolve credentials, contact external APIs from the scan path, read secret values, modify IAM, block traffic, or enforce generated policies. Policy drafts always require human review. + +### Verification + +The release workflow runs the full test suite, race tests, `go vet`, fuzz payload validation, and the long-running runtime fuzz gates before GoReleaser publishes platform archives and checksums. + +## [0.1.0-alpha.2] + +Previous public alpha release. diff --git a/README.md b/README.md index b8d915f..adf45b7 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,23 @@ go run ./cmd/agentctl scan ./workspace --format json --output report.json go run ./cmd/agentctl scan ./workspace --format sarif --fail-on high --output agentctl.sarif ``` +Runtime-аудит принимает metadata-only JSONL, OpenTelemetry JSON или API Gateway events: + +```bash +go run ./cmd/agentctl runtime-audit events.jsonl \ + --inventory static-report.json --format sarif --fail-on high \ + --output runtime.sarif +go run ./cmd/agentctl runtime-audit events.jsonl \ + --inventory static-report.json --format json --output runtime.json +go run ./cmd/agentctl runtime-diff before-runtime.json after-runtime.json \ + --format html --output runtime-diff.html +``` + +Runtime input is intentionally metadata-only. Prompts, tool arguments, request and +response bodies, authorization headers, tokens and secrets are rejected or omitted. +Runtime findings can be compared with a JSON baseline and can use expiring +reviewed suppressions in the same way as static findings. + For incremental CI gates, keep a reviewed JSON report and suppress findings that were already accepted in that baseline: @@ -129,6 +146,17 @@ ACP-005 is emitted only for MCP policy sources such as `.mcp.json`, Source-code references to an MCP server are inventory evidence but do not create a policy finding by themselves. +The runtime audit correlates observed agent activity with that inventory. It supports +streaming JSONL aggregation, OpenTelemetry/API Gateway normalization, runtime SARIF, +stable baselines and expiring suppressions. `runtime-diff` compares snapshots and +exports text, JSON, CSV or HTML. + +The read-only connector metadata layer normalizes already-fetched GitHub, GitLab, +Docker and Kubernetes metadata. Cloud audit normalization supports AWS, GCP and +Azure event shapes, and policy generation produces review-only `policy-draft.v1` +objects from observed usage. These layers do not call external APIs, resolve +credentials, read secret values, change IAM, block traffic or enforce policies. + The default policy excludes common non-production paths such as tests, examples, samples, tutorials, fixtures, documentation and schemas. Agent definitions under tooling directories such as `.claude` and `.github`, Markdown agent files, and framework library layouts remain scannable when they contain strong agent signals. Use `.agentctl/config.yaml` to add workspace-specific exclusions and approved providers or MCP servers. `agentctl init` never overwrites an existing policy. ## Risk rules @@ -164,11 +192,12 @@ The alpha includes ten explainable rules: - Detection is heuristic and should be reviewed by a human. - 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. +- No runtime proxy, IAM remediation, hosted control plane, dashboard, issue export or compliance certification is included. Cloud policy output is a draft and requires human review. - 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. SARIF is available now. Use `--fail-on high` or `--fail-on critical` to make findings a CI gate; the default `none` keeps scans informational. Use `agentctl --help`, `agentctl scan --help`, or `agentctl init --help` for the supported command surface. +Use `agentctl runtime-audit --help` and `agentctl runtime-diff --help` for runtime commands. ### GitHub Actions example From ac60f02d0398793da3dd6b809434364c3f21a07a Mon Sep 17 00:00:00 2001 From: Manus AI Date: Sat, 12 Sep 2026 14:33:10 +0000 Subject: [PATCH 5/5] ci: run required checks for release documentation --- .github/workflows/adversarial-fuzz.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/adversarial-fuzz.yml b/.github/workflows/adversarial-fuzz.yml index 1300641..2ad4445 100644 --- a/.github/workflows/adversarial-fuzz.yml +++ b/.github/workflows/adversarial-fuzz.yml @@ -10,6 +10,8 @@ on: - "internal/connectors/**" - "internal/runtimefuzz/**" - "docs/connectors.md" + - "docs/**" + - "README.md" - "testdata/adversarial/**" - "testdata/fuzz/**" - ".github/workflows/adversarial-fuzz.yml"