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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ OpenSourceOM Core is the platform behind [opensourceom.org](https://opensourceom

Traditional scanners flood you with CVEs and misconfigurations. OpenSourceOM connects the dots — showing which findings sit on paths from the internet to your sensitive data and privileged identities.

> **Status:** Early development (Phase 3). CSPM rules, identity blast radius, Kubernetes ingest, exports, a collector plugin SDK, and a Helm chart are available. CVE findings follow package and image inventory. Datastores can carry a sensitivity mark from a tag or label. A rules run writes an attack-path finding when a workload finding sits on a path to a datastore. Current work is cloud audit ingest, with further rule packs open for contributors. See the [roadmap](./docs/ROADMAP.md).
> **Status:** Early development (Phase 3). CSPM rules, identity blast radius, Kubernetes ingest, exports, a collector plugin SDK, and a Helm chart are available. CVE findings follow package and image inventory. Datastores can carry a sensitivity mark from a tag or label. A rules run writes an attack-path finding when a workload finding sits on a path to a datastore. `om scan aws` attaches recent CloudTrail management events to the identity and resource on that path. Further rule packs are open for contributors. See the [roadmap](./docs/ROADMAP.md).

## Why this exists

Expand Down Expand Up @@ -160,7 +160,7 @@ Full documentation: [opensourceom.org](https://opensourceom.org) (docs at [opens
| **0** | Graph schema v0, AWS collector, ingest API, `om` CLI |
| **1** | Attack path queries, CVE enrichment, web UI, Azure/GCP collectors |
| **2** | CSPM rules, blast radius, K8s connector, exports |
| **3** *(now)* | Graph accuracy, crown-jewel datastores, attack-path findings, rule packs, cloud audit ingest |
| **3** *(now)* | Graph accuracy, crown-jewel datastores, attack-path findings, CloudTrail context, rule packs |

Details: [docs/ROADMAP.md](./docs/ROADMAP.md)

Expand Down
2 changes: 1 addition & 1 deletion collectors/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Cloud and platform ingestion plugins. Each collector normalizes provider APIs in

The **demo** collector loads a fixed environment (internet-exposed web tier, production database, public/private buckets, admin vs app identities, public Kubernetes service). Use it to exercise CSPM packs without cloud credentials.

The **AWS** collector emits properties the CIS pack matches on: `open_ingress`, `imdsv2`, `public_ip`, S3 `encryption` / `versioning` / `public_access_block`, and IAM user `mfa` / `unused_access_keys`. It also records RDS and Aurora instances as datastores. S3 bucket tags and the RDS `TagList` copy `sensitivity` or `data-class` onto the datastore. `sensitivity` wins when both are set.
The **AWS** collector emits properties the CIS pack matches on: `open_ingress`, `imdsv2`, `public_ip`, S3 `encryption` / `versioning` / `public_access_block`, and IAM user `mfa` / `unused_access_keys`. It also records RDS and Aurora instances as datastores. S3 bucket tags and the RDS `TagList` copy `sensitivity` or `data-class` onto the datastore. `sensitivity` wins when both are set. The same scan reads CloudTrail management events from the last 24 hours and stores the ones that name an identity and a resource on an exposed path. A failed lookup omits those events.

The **Azure** collector records logical SQL servers, and the **GCP** collector records Cloud SQL instances. A workload gets `CAN_ACCESS` only when a security group, firewall, or VPC path allows it. Azure copies the crown-jewel mark from storage-account and SQL-server tags. GCP copies it from a bucket label or a Cloud SQL user label. The keys are `sensitivity` and `data-class`. A plugin may set `sensitivity` on a datastore directly.

Expand Down
4 changes: 2 additions & 2 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ SPDX-License-Identifier: Apache-2.0

| Component | Location | Notes |
|-----------|----------|-------|
| **Collectors** | `internal/collectors/` | AWS, Azure, GCP, Kubernetes, demo; RDS, Azure SQL, and Cloud SQL are datastores; AWS emits CIS pack properties |
| **Collectors** | `internal/collectors/` | AWS, Azure, GCP, Kubernetes, demo; RDS, Azure SQL, and Cloud SQL are datastores; AWS emits CIS pack properties and recent CloudTrail management events |
| **Plugin SDK** | `sdk/collector`, `internal/plugins/` | External executables; `om scan plugin` |
| **Graph store** | `internal/graph/`, `migrations/` | PostgreSQL `nodes` + `edges` |
| **Path queries** | `internal/graph/query.go` | Named queries including `internet-to-datastore` and `internet-to-sensitive-datastore` |
Expand Down Expand Up @@ -78,7 +78,7 @@ Making the skeleton true, in order:
- CVE enrichment tied to workload inventory. `om enrich cve` writes a finding only when a workload package or image matches.
- Crown-jewel mark on datastores. A tag or label named `sensitivity` or `data-class` is stored on the datastore, and `internet-to-sensitive-datastore` keeps only those paths.
- Attack path as the finding. A rules run writes one `attack_path` finding per existing finding on an internet-reachable workload that can reach a datastore, and stores the path as ordered node ids.
- Cloud audit logs as graph context
- Cloud audit logs as graph context. `om scan aws` stores CloudTrail management events from the last 24 hours on the identity and resource they name, when that resource is on an exposed path. `GET /v1/graph/query` returns those events in `audits` for each path that contains the resource. Azure Activity Log, GCP Cloud Audit Logs, and S3 data events such as `GetObject` are not collected.

Further community rule packs (PCI and additional CIS mappings) stay open for contributors.

Expand Down
8 changes: 5 additions & 3 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ High-level plan for OpenSourceOM core. Timelines are approximate and community-d
- [x] Sample environment (`om scan demo`)
- [ ] Broader community rule packs (PCI and additional CIS mappings) — [#10](https://github.com/OpenSourceOM/core/issues/10)

Phases 0–2 shipped the walking skeleton. Exposure and identity edges now follow the cloud and Kubernetes. CVE findings follow package and image inventory on the workload. Datastores carry a sensitivity mark when a tag or label names one. A rules run writes an attack-path finding for each finding already on an internet-reachable workload that can reach a datastore. Current work is cloud audit logs as graph context.
Phases 0–2 shipped the walking skeleton. Exposure and identity edges now follow the cloud and Kubernetes. CVE findings follow package and image inventory on the workload. Datastores carry a sensitivity mark when a tag or label names one. A rules run writes an attack-path finding for each finding already on an internet-reachable workload that can reach a datastore. `om scan aws` attaches recent CloudTrail management events to the identity and resource they name, and path queries return an event when that resource is on the path.

Correctness and operability come first:

Expand All @@ -50,9 +50,11 @@ The path, in order:
- [x] **CVE enrichment tied to workload inventory** — [#12](https://github.com/OpenSourceOM/core/issues/12)
- [x] **Crown-jewel mark on datastores** — [#58](https://github.com/OpenSourceOM/core/issues/58)
- [x] **Attack path as the finding** — [#57](https://github.com/OpenSourceOM/core/issues/57)
- **Cloud audit logs as graph context** — [#33](https://github.com/OpenSourceOM/core/issues/33)
- [x] **Cloud audit logs as graph context** — [#33](https://github.com/OpenSourceOM/core/issues/33)

[#10](https://github.com/OpenSourceOM/core/issues/10) stays open for a contributor who wants another rule pack. The priority above is graph accuracy.
`om scan aws` reads CloudTrail management events from the last 24 hours. An event is stored on the identity and the resource it names when that resource sits on an exposed path. Named path queries list those events when the resource is on the returned path. Azure Activity Log, GCP Cloud Audit Logs, and S3 object data events such as `GetObject` are not in this slice.

[#10](https://github.com/OpenSourceOM/core/issues/10) stays open for a contributor who wants another rule pack.

## Open source vs. commercial

Expand Down
1 change: 1 addition & 0 deletions docs/adr/001-graph-schema-v0.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ Synthetic nodes use stable global IDs (e.g. `internet:global`).
- **Heuristics:** AWS `admin_access` follows attached and inline policies: an allow of `*` on `*`, or the AWS-managed `AdministratorAccess` policy. Deny statements, conditions, permission boundaries, and group policies are not evaluated. Azure `admin_access` is Owner, Contributor, User Access Administrator, or a custom role whose actions are `*` or include `Microsoft.Authorization/roleAssignments/write`. GCP `admin_access` is `roles/owner`, `roles/editor`, `roles/resourcemanager.projectIamAdmin`, or a custom role that includes `resourcemanager.projects.setIamPolicy`. Azure `CAN_ACCESS` for storage follows role-assignment scope. GCP `CAN_ACCESS` for buckets follows project and bucket IAM. Conditional assignments and bindings are not treated as access. Those identity edges do not apply to managed databases. Azure and GCP `REACHABLE` requires an internet path and an allow. A path is a public IP, or a private address behind a load balancer with a public frontend. GCP allow is an ingress firewall rule from `0.0.0.0/0` or `::/0` that selects the instance and is not covered by a higher-priority deny; no matching allow stays closed. Azure allow is an inbound NSG rule from `Internet`, `*`, or `0.0.0.0/0` that is not covered by a higher-priority deny. When both a NIC and a subnet NSG are attached, both must allow. A VM with a public IP and no NSG stays reachable, which is Azure's platform default. Network endpoint groups are not expanded. Kubernetes `REACHABLE` follows a LoadBalancer service or an Ingress backend that selects the pod. NodePort alone does not. A NetworkPolicy that selects the pod and governs ingress removes that edge unless a rule allows every source or `0.0.0.0/0` / `::/0`. Gateway API and internal-only ingress classes are not modeled.
- **Crown jewels:** A datastore may carry `sensitivity`. Collectors copy it from a resource tag or label named `sensitivity` or `data-class` when the provider returns one. `sensitivity` wins when both are present. A blank value is omitted. Plugins may set the property on the node. Object contents are not read. `internet-to-datastore` stays unfiltered. `internet-to-sensitive-datastore` is that walk restricted to datastores whose `sensitivity` is a non-empty string.
- **Attack-path findings:** A rules run writes one `Finding` per existing finding on an internet-reachable workload paired with a datastore that workload can reach. Reach is a `CAN_ACCESS` edge from the workload, or `ASSUMES` to an identity that has `CAN_ACCESS`. The finding's `finding_type` is `attack_path`. Its `path` property is ordered node ids: the shortest `REACHABLE` walk from the internet to the workload, the identity when the hop uses one, then the datastore. `GET /v1/findings` returns that list on the row. A reachable workload with no datastore hop does not get this finding. A hop whose workload has no other finding does not either. `sensitivity` does not filter these rows. Per-control CSPM findings still run.
- **Cloud audit context:** `om scan aws` calls CloudTrail `LookupEvents` for the scan region, and also `us-east-1` when the scan region is different, over the last 24 hours. A fixed list of management events is kept when the event names an identity already in the batch and a resource on an exposed path. Exposed means an internet-reachable workload, a node that workload assumes or can access, a network that workload affects, a public datastore, or an identity that can access a public datastore. The event is stored as `audit_events` on both nodes, at most five per node, newest first. A failed lookup omits the property. `GET /v1/graph/query` and `om paths run` include an `audits` entry when the event's resource node is on a returned path. S3 object data events such as `GetObject` are not returned by `LookupEvents`. Azure and GCP audit logs are not collected. These events are cloud-provider evidence, not a log of who used OpenSourceOM.
- **Managed databases:** RDS (including Aurora instances), Azure SQL servers, and Cloud SQL instances are `Datastore` nodes. `public_access` is true only when the endpoint is open to the internet: RDS is publicly accessible and a security group allows the instance port from `0.0.0.0/0` or `::/0`; an Azure SQL server has public network access and a firewall rule from `0.0.0.0` to `255.255.255.255`; Cloud SQL has a public IPv4 address and an authorized network of `0.0.0.0/0` or `::/0`. Workload `CAN_ACCESS` follows that network path. RDS requires the same VPC and a security group that references the workload or contains its private IPv4 address on the instance port. Azure SQL requires a virtual-network rule for the VM's subnet, or a firewall range that contains the VM's public IP while the public endpoint is enabled. Cloud SQL requires the instance's private-IP network, or an authorized network that contains the instance's public IP. Same-account membership is not enough. VPC peering, prefix lists, Azure private endpoints, default outbound SNAT, and Cloud SQL Private Service Connect are not expanded.

## References
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/storage/armstorage/v3 v3.0.0
github.com/aws/aws-sdk-go-v2 v1.47.1
github.com/aws/aws-sdk-go-v2/config v1.33.6
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.53.0
github.com/aws/aws-sdk-go-v2/service/ec2 v1.336.1
github.com/aws/aws-sdk-go-v2/service/iam v1.64.1
github.com/aws/aws-sdk-go-v2/service/rds v1.129.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@ github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.4 h1:dD4MR81I7YkpEBRk6UP
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.4/go.mod h1:EcXV1kAFd5XwSkDHlj94gnF3q5CkJyYiIJfH8N0VmrE=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.4 h1:7Wo47d/xn/7KttCSBd8EGYeZ7ULRFRkUHr6vkZPBzVQ=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.4/go.mod h1:tDB2IVC1xC3vX8o+6uRlzhTxP3g1b77CZXFX/oD2FnQ=
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.53.0 h1:WMgigsEPtSgsVe+jBMqCuAF2u0j/CnSjCm3I6Ar7nFo=
github.com/aws/aws-sdk-go-v2/service/cloudtrail v1.53.0/go.mod h1:1QQJFpFapuZD93JdP+VNezwfQt88oyxqW6bdCC5xmbo=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.336.1 h1:qiuU5+MtLJV2CAxLZYA/GPuvrsScBIk2am+QNAoHmMM=
github.com/aws/aws-sdk-go-v2/service/ec2 v1.336.1/go.mod h1:d0e0acsyS3WnFCFJiByGwnUgPpn2wAk97PTIksHN2NI=
github.com/aws/aws-sdk-go-v2/service/iam v1.64.1 h1:Uwitin0mXJ7iG5rFuuja3aG9/c84LpyyZUhaTiwZj7w=
Expand Down
16 changes: 13 additions & 3 deletions internal/api/web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -196,7 +196,12 @@ function escapeHTML(value) {
.replaceAll(">", ">");
}

function renderPathDetail(paths, edges, truncation) {
function auditsForPath(audits, index) {
const row = (audits || []).find((item) => item.index === index);
return row?.events || [];
}

function renderPathDetail(paths, edges, truncation, audits) {
const panel = document.getElementById("path-detail");
if (!paths.length && !truncation) {
panel.classList.add("hidden");
Expand All @@ -218,8 +223,13 @@ function renderPathDetail(paths, edges, truncation) {
`</li>`,
);
}
const events = auditsForPath(audits, index).map((event) => {
const bits = [event.time, event.name, event.principal, event.resource].filter(Boolean);
return `<li>${escapeHTML(bits.join(" "))}</li>`;
}).join("");
const eventList = events ? `<p class="meta">CloudTrail</p><ul>${events}</ul>` : "";
const names = path.map((node) => escapeHTML(node.name)).join(" → ");
return `<p><strong>Path ${index + 1}.</strong> ${names}</p><ol>${hops.join("")}</ol>`;
return `<p><strong>Path ${index + 1}.</strong> ${names}</p><ol>${hops.join("")}</ol>${eventList}`;
}).join("");
}

Expand Down Expand Up @@ -290,7 +300,7 @@ async function refresh() {
if (query) {
const result = await fetchJSON(`/v1/graph/query?name=${encodeURIComponent(query)}`);
const paths = result.paths || [];
renderPathDetail(paths, snapshot.edges, result.truncated ? result.truncation : "");
renderPathDetail(paths, snapshot.edges, result.truncated ? result.truncation : "", result.audits);
const built = pathGraph(paths, snapshot.edges);
renderGraph(built.nodes, built.edges);
return;
Expand Down
14 changes: 14 additions & 0 deletions internal/cmd/paths.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,22 @@ var pathsRunCmd = &cobra.Command{
fmt.Println("No paths found.")
return nil
}
audits := map[int][]graph.AuditEvent{}
for _, audit := range result.Audits {
audits[audit.Index] = audit.Events
}
for i, path := range result.Paths {
fmt.Printf("%d. %s\n", i+1, graph.FormatPath(path))
for _, event := range audits[i] {
fmt.Printf(" %s %s", event.Time, event.Name)
if event.Principal != "" {
fmt.Printf(" %s", event.Principal)
}
if event.Resource != "" {
fmt.Printf(" → %s", event.Resource)
}
fmt.Println()
}
}
return nil
},
Expand Down
2 changes: 1 addition & 1 deletion internal/cmd/scan.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ var scanCmd = &cobra.Command{

var scanAWSCmd = &cobra.Command{
Use: "aws",
Short: "Scan the current AWS account (EC2, IAM, S3, security groups)",
Short: "Scan the current AWS account (EC2, IAM, S3, security groups) and recent CloudTrail management events",
RunE: func(cmd *cobra.Command, args []string) error {
cfg := loadConfig()
collector, err := aws.NewCollector(cmd.Context(), cfg.AWSRegion)
Expand Down
Loading
Loading