diff --git a/integrations/oracle_cloud_oci/README.md b/integrations/oracle_cloud_oci/README.md new file mode 100644 index 0000000..e69242f --- /dev/null +++ b/integrations/oracle_cloud_oci/README.md @@ -0,0 +1,632 @@ +# Oracle Cloud Infrastructure (OCI)-Wazuh Integration + +## Table of Contents + +* [Introduction](#introduction) +* [Prerequisites](#prerequisites) +* [Installation and Configuration](#installation-and-configuration) + * [Installing Oracle Cloud Infrastructure](#installing-oracle-cloud-infrastructure) + * [Initial Oracle Cloud Infrastructure Configuration](#initial-oracle-cloud-infrastructure-configuration) + * [Installing Wazuh](#installing-wazuh) + * [Initial Wazuh Configuration](#initial-wazuh-configuration) + * [Using the Integration Files](#using-the-integration-files) +* [Integration Steps](#integration-steps) +* [Integration Testing](#integration-testing) +* [Service Coverage Reference](#service-coverage-reference) +* [Troubleshooting](#troubleshooting) +* [Provenance and Maintenance](#provenance-and-maintenance) +* [Sources](#sources) + +--- + +### Introduction + +Wazuh ships native cloud modules for AWS, Azure and GCP, but **there is no native OCI +module**. This integration fills that gap for Oracle Cloud Infrastructure. + +The data path Oracle supports is: + +``` + OCI service Logging / Audit Connector Hub this integration +┌───────────────┐ ┌────────────────┐ ┌──────────────┐ ┌──────────────────┐ +│ VCN │ │ │ │ │───────▶│ Streaming │ +│ Object Storage│────▶│ Service Logs │─────▶│ Connector │ │ consumer group │──┐ +│ API Gateway │ │ │ │ │ └──────────────────┘ │ +│ Email Delivery│ ├────────────────┤ │ (optional │ │ +├───────────────┤ │ │ │ log filter │ ┌──────────────────┐ │ +│ DRG │ │ Audit Logs │ │ task) │───────▶│ Object Storage │ │ +│ Block Storage │────▶│ (_Audit, │─────▶│ │ │ bucket poller │──┤ +│ ADB / ATP │ │ every service)│ │ │ └──────────────────┘ │ +│ APEX │ │ │ └──────────────┘ │ +│ Data Integr. │ └────────────────┘ │ +│ Integration │ oci-logs.py │ +└───────────────┘ │ + ▼ + ┌──────────────────────────────────────────────────────────┐ + │ Wazuh manager │ + │ analysisd socket ──or── JSON file + │ + │ rules 112000-112099 │ + └──────────────────────────────────────────────────────────┘ +``` + +> **Other OCI integrations in this repository.** Two others exist and this replaces +> neither. `Oracle_Cloud_Infrastructure_streaming/` routes OCI Streaming through +> Logstash and Kafka and ships a dashboard — use it if you already run Logstash. +> `Oracle-Integration-(OCI)/` is an earlier Streaming-only collector that ships no +> rules. This one talks to OCI directly through the SDK, covers both delivery +> targets, and brings the ruleset. The rules file is named +> `oracle_cloud_oci_rules.xml` so it cannot overwrite theirs in +> `/var/ossec/etc/rules/`. + +The `oci-logs.py` collector is the last hop. It reads the log records Connector Hub +delivered, normalises them into a stable JSON shape, and forwards them to Wazuh — +either straight to the analysisd socket or as JSON lines on disk for a `` +block to tail. + +**What this buys you** + +* One collector covers **every OCI service**, because OCI Audit records the control + plane for all of them. Services that also emit data-plane service logs (VCN flow + logs, Object Storage access, API Gateway access, Email Delivery) flow through the + same connector. +* Consistent field names. OCI's raw records nest differently per service; the + collector hoists `srcip`, `srcuser`, `action`, `status`, `url`, `srcport`, + `dstip`, `dstport` and `protocol` to the top level so the Wazuh alert schema and + the dashboards' standard filters work without per-service rule gymnastics. +* 35 rules in the `112000-112099` range, covering IAM changes, security list and + route table edits, DRG teardown, volume and bucket deletion, public-bucket + exposure, ADB/ATP stop and credential rotation, rejected VCN flows, API Gateway + auth failures and Email Delivery bounce storms. Every rule has been exercised + against a running manager -- see [Integration Testing](#integration-testing). +* An offline replay mode (`--source local`) so the whole normalise-and-alert chain + can be validated with `wazuh-logtest` before you have a tenancy wired up. + +--- + +### Prerequisites + +**Wazuh** + +* Wazuh manager 4.4 or later. Rules use `` on JSON-decoded events, + `same_field`, and ``. +* The collector runs on the **manager** (writing to analysisd) or on any **agent** + (writing to a file that `` tails). It does not need to run inside OCI. + +**Oracle Cloud Infrastructure** + +* A tenancy with the Logging, Audit and Connector Hub services enabled. Audit is on + by default in every compartment. +* Either an **OCI Streaming** stream or an **Object Storage** bucket to receive the + Connector Hub output. +* An IAM user with an API signing key, **or** — preferred — a compute instance in a + dynamic group so the collector can use instance principals and keep no private key + on disk. + +**Host** + +* Python 3.8+. The Wazuh manager embeds a suitable interpreter at + `/var/ossec/framework/python/bin/python3`, which is what the shebang targets. +* The `oci` SDK (`pip install -r requirements.txt`). Not needed for `--source local`. +* Outbound HTTPS (443) to the OCI service endpoints in your region. + +**IAM permissions for the collector user or dynamic group** + +For an Object Storage bucket: + +``` +Allow group WazuhCollectors to read objects in compartment where target.bucket.name = '' +Allow group WazuhCollectors to read buckets in compartment +``` + +Add this only if you run the collector with `--delete-after-read`: + +``` +Allow group WazuhCollectors to manage objects in compartment where all {target.bucket.name = '', request.permission = 'OBJECT_DELETE'} +``` + +For a stream: + +``` +Allow group WazuhCollectors to use stream-pull in compartment where target.stream.id = '' +Allow group WazuhCollectors to read streams in compartment +``` + +Separately, the **connector itself** needs permission to write to its target. The +Console offers these as default policies when you create the connector; accept them +or write the equivalent custom policy with a dynamic group. + +--- + +### Installation and Configuration + +#### Installing Oracle Cloud Infrastructure + +OCI is a hosted service; nothing to install. You need an active tenancy and +Console access with administrator rights in the compartments you want to monitor. + +#### Initial Oracle Cloud Infrastructure Configuration + +**Step 1 — Turn on the logs you want.** + +*Audit* is already on for every service and needs no setup. It lands in the `_Audit` +log group of each compartment and is what covers DRG, Block Storage, ADB/ATP, APEX, +Data Integration and Integration Service. See +[Service Coverage Reference](#service-coverage-reference) for which of your services +also have data-plane service logs. + +*Service logs* must be enabled per resource. In the Console go to +**Observability & Management > Logging > Logs > Enable service log**, pick the +resource, and pick a log category: + +| Service | Log category to enable | +|---|---| +| VCN | Flow Logs (on the subnet or VNIC) | +| Object Storage | Read Access Events, Write Access Events (on the bucket) | +| API Gateway | Access, Execution (on the deployment) | +| Email Delivery | Outbound accepted / relayed / suppressed | + +> If the **Enable service log** dialog offers no categories for a resource, that +> service has no data-plane log in your region and Audit is the whole story for it. +> Check this per service rather than assuming — Oracle adds categories over time. + +**Step 2 — Create the delivery target.** + +Choose one: + +* **Object Storage** (simpler, cheaper, minutes of latency): create a bucket, e.g. + `wazuh-oci-logs`. Set a lifecycle rule to delete objects after a few days, or run + the collector with `--delete-after-read`. +* **Streaming** (near real time, server-side offset tracking): create a stream, e.g. + `wazuh-oci-logs`, with a retention long enough to survive a collector outage — + 24 hours or more is sensible. + +**Step 3 — Create the connector.** + +**Analytics & AI > Messaging > Connector Hub > Create connector**. + +* **Source**: Logging. +* **Compartment**: the one you want to collect from. +* **Log group**: `_Audit` for audit, or the log group holding your service logs. Add + a second source entry per log group — one connector can read several. + Tick *Include subcompartments* to cover a compartment tree in one connector. +* **Task** (optional): a Log Filter task drops noise before it reaches Wazuh. Filtering + out `ACCEPT` flow-log records is the highest-value filter in most tenancies. +* **Target**: the bucket or stream from step 2. +* Accept the default IAM policies the Console offers, or supply your own. + +> Connector Hub reads log data from the connector's creation time forward, and the +> Logging source retains 24 hours. Create the connector before you need the data. +> Editing a connector's source or target **resets** it internally, which can replay +> or skip records — create a new connector instead of editing a working one. + +**Step 4 — Credentials for the collector.** + +If the collector runs on an OCI compute instance, skip this: add the instance to a +dynamic group and use `--auth instance_principal`. + +Otherwise create an IAM user, generate an API signing key +(**Identity > Users > > API Keys > Add API Key**), and fill in +[config.example](config.example) with the user OCID, fingerprint, key path, tenancy +OCID and region. + +#### Installing Wazuh + +A standard installation is assumed. Follow the +[Wazuh installation guide](https://documentation.wazuh.com/current/installation-guide/index.html). +This integration needs no special build options or extra modules. + +#### Initial Wazuh Configuration + +None beyond a working manager. The collector uses the standard analysisd queue socket +at `/var/ossec/queue/sockets/queue`, or writes a file for the standard log collector. + +#### Using the Integration Files + +| File | Purpose | Install to | +|---|---|---| +| `oci-logs.py` | The collector | `/var/ossec/wodles/oci/oci-logs.py` | +| `ruleset/rules/oracle_cloud_oci_rules.xml` | Rules 112000-112099 | `/var/ossec/etc/rules/` | +| `config.example` | OCI API-key template | `/var/ossec/wodles/oci/config` | +| `systemd/wazuh-oci-logs.service` | Runs one collection pass | `/etc/systemd/system/` | +| `systemd/wazuh-oci-logs.timer` | Schedules the passes | `/etc/systemd/system/` | +| `sample_logs.txt` | Representative records for testing | anywhere | + +**Deploy the collector** + +```bash +sudo mkdir -p /var/ossec/wodles/oci +sudo cp oci-logs.py /var/ossec/wodles/oci/ +sudo chown root:wazuh /var/ossec/wodles/oci/oci-logs.py +sudo chmod 750 /var/ossec/wodles/oci/oci-logs.py + +sudo /var/ossec/framework/python/bin/python3 -m pip install -r requirements.txt +``` + +**Deploy the credentials** (skip if using instance principals) + +```bash +sudo install -m 0600 -o root -g root config.example /var/ossec/wodles/oci/config +sudo install -m 0600 -o root -g root ~/oci_api_key.pem /var/ossec/wodles/oci/ +sudo "${EDITOR:-vi}" /var/ossec/wodles/oci/config # fill in your OCIDs +``` + +**Deploy the rules** + +```bash +sudo cp ruleset/rules/oracle_cloud_oci_rules.xml /var/ossec/etc/rules/ +sudo chown root:wazuh /var/ossec/etc/rules/oracle_cloud_oci_rules.xml +sudo chmod 660 /var/ossec/etc/rules/oracle_cloud_oci_rules.xml +sudo systemctl restart wazuh-manager +``` + +No custom decoder is needed. The collector emits JSON, so Wazuh's built-in JSON +decoder parses it and every field the rules reference is addressable directly. + +**Schedule the collector** + +```bash +sudo cp systemd/wazuh-oci-logs.{service,timer} /etc/systemd/system/ +sudo "${EDITOR:-vi}" /etc/systemd/system/wazuh-oci-logs.service # set your bucket or stream +sudo systemctl daemon-reload +sudo systemctl enable --now wazuh-oci-logs.timer +``` + +Cron works equally well if you would rather not use systemd: + +``` +* * * * * /var/ossec/wodles/oci/oci-logs.py --source objectstorage --bucket wazuh-oci-logs --config-file /var/ossec/wodles/oci/config >> /var/ossec/logs/oci-logs.log 2>&1 +``` + +--- + +### Integration Steps + +#### Option A — Object Storage (recommended for most deployments) + +Connector Hub writes batches of log records into the bucket. The collector lists the +bucket in name order, reads each new object, and checkpoints the last object name to +`--state-file` so restarts resume cleanly. + +```bash +/var/ossec/wodles/oci/oci-logs.py \ + --source objectstorage \ + --bucket wazuh-oci-logs \ + --config-file /var/ossec/wodles/oci/config \ + --output analysisd +``` + +Useful flags: `--prefix` to read one connector's output from a shared bucket, +`--start-time 2026-07-01` to ignore a backlog on first run, and `--delete-after-read` +to drain the bucket as you go. + +#### Option B — Streaming (lowest latency) + +The collector consumes with a **consumer group**, so OCI tracks the read offset +server-side and a restart picks up where the last run stopped. + +```bash +/var/ossec/wodles/oci/oci-logs.py \ + --source streaming \ + --stream-id ocid1.stream.oc1.iad.EXAMPLE \ + --config-file /var/ossec/wodles/oci/config \ + --output analysisd +``` + +The messages endpoint is looked up automatically from the stream OCID; pass +`--stream-endpoint` to skip the lookup. Change `--group-name` to re-read a stream +from the beginning. Run exactly one process per `--instance-name`. + +#### Option C — write to a file and let `` tail it + +This is the shape Wazuh support describes, and the right choice when the collector +runs on an **agent** rather than the manager. + +```bash +/var/ossec/wodles/oci/oci-logs.py \ + --source objectstorage --bucket wazuh-oci-logs \ + --config-file /var/ossec/wodles/oci/config \ + --output file --path /var/ossec/logs/oci/oci.json +``` + +Then in `ossec.conf`: + +```xml + + json + /var/ossec/logs/oci/oci.json + +``` + +Rotate that file — the collector appends and never truncates. + +#### The event Wazuh receives + +Each OCI record becomes one JSON event with a stable shape: + +```json +{ + "integration": "oci", + "oci": { + "log_type": "audit", + "service": "objectstorage", + "type": "com.oraclecloud.objectstorage.DeleteBucket", + "time": "2026-07-01T08:01:00.000Z", + "id": "a1b2c3d4-0000-4000-8000-000000000002", + "data": { "...the untouched OCI record..." }, + "compartment_id": "ocid1.compartment.oc1..aaaaaaaaprod", + "log_group_id": "_Audit", + "tenant_id": "ocid1.tenancy.oc1..aaaaaaaatenancyexample", + "compartment_name": "prod", + "resource_name": "finance-archive", + "resource_id": "ocid1.bucket.oc1..aaaaaaaafinance", + "srcip": "198.51.100.77", + "srcuser": "contractor@acme.com", + "action": "DeleteBucket", + "status": "204", + "url": "/n/acmens/b/finance-archive" + }, + "srcip": "198.51.100.77", + "srcuser": "contractor@acme.com", + "action": "DeleteBucket", + "status": "204", + "url": "/n/acmens/b/finance-archive" +} +``` + +**Why some values appear twice.** `srcip`, `srcuser`, `action`, `status`, `url` and +friends are emitted both at the top level and mirrored inside `oci`. That is +deliberate. At the top level they land on Wazuh's *static* decoder fields, which is +what populates `data.srcip` in the alert and what `` correlates on. +But analysisd matches static fields with a plain `strcmp` for `` and with +OSMatch for `` — neither supports the regex the rules need, and `` is rejected outright because the name is reserved. The mirror under +`oci.*` is a dynamic field, and `` on a dynamic name gets full OS_Regex. Write +new rules against `oci.action` / `oci.status`, not the bare names. + +`oci.status` is always present, falling back to `unknown` when the source record +carries no status. Rules gate on it with `negate="yes"`, which does not match a +missing field, so a record without one would otherwise drop out of the ruleset. + +`oci.log_type` is one of `audit`, `vcn_flow`, `objectstorage`, `apigateway_access`, +`apigateway_execution`, `emaildelivery`, `data_integration`, `integration_service`, +`database`, `loadbalancer`, `waf`, `functions`, `oke`, or `oci` for anything +unrecognised. The original record is always preserved under `oci.data`, so a rule can +reach any field Oracle sends even if the collector does not hoist it. + +Sending to analysisd uses the header `1:oci:`, so alerts carry `location: oci`. + +--- + +### Integration Testing + +#### 1. Offline — no tenancy required + +This validates the collector's parsing and normalisation, and every rule field path, +without touching OCI: + +```bash +cd integrations/oracle_cloud_oci +python3 oci-logs.py --source local --input sample_logs.txt --output stdout +``` + +Expected: 21 JSON events on stdout and `Forwarded 21 event(s) via stdout` on stderr. + +Now push them through the ruleset: + +```bash +python3 oci-logs.py --source local --input sample_logs.txt --output stdout \ + | sudo /var/ossec/bin/wazuh-logtest -q +``` + +Every row below was confirmed on Wazuh 4.14.7 — each sample lands on exactly this +rule and level: + +| Sample record | Rule | Level | +|---|---|---| +| `CreateUser` succeeding | 112005 | 10 | +| `CreateUser` denied with 403 | 112003 | 8 | +| `DeletePolicy` | 112006 | 12 | +| `UpdateSecurityList` opening SSH to 0.0.0.0/0 | 112007 | 10 | +| `DeleteDrgAttachment` | 112009 | 12 | +| `DeleteVolume` | 112011 | 10 | +| `DeleteBucket` | 112012 | 12 | +| `UpdateBucket` with `publicAccessType: ObjectRead` | 112013 | 12 | +| `StopAutonomousDatabase` | 112015 | 10 | +| `ChangeAutonomousDatabaseAdminPassword` | 112016 | 10 | +| `UpdateIntegrationInstance` | 112017 | 5 | +| `DeleteIntegrationInstance` | 112018 | 10 | +| VCN flow log `REJECT` to port 22 | 112021 | 4 | +| Object Storage `DELETE` of `payroll.csv` | 112031 | 6 | +| Object Storage `GET` returning 403 | 112032 | 8 | +| API Gateway 401 | 112042 | 8 | +| API Gateway 500 | 112044 | 7 | +| Email Delivery `SUPPRESSED` / `HARDBOUNCE` | 112051 | 5 | + +The VCN flow log `ACCEPT` sample deliberately stops at 112020, level 0 — accepted +traffic is recorded but does not alert. + +The five frequency rules need repeated events inside their timeframe, so a single +pass over the samples will not fire them. Replay one record in a loop instead: + +```bash +grep REJECT sample_logs.txt > /tmp/reject.json +for i in $(seq 1 25); do cat /tmp/reject.json; done > /tmp/burst.json +python3 oci-logs.py --source local --input /tmp/burst.json --output analysisd +``` + +Then look for the aggregate rule in `/var/ossec/logs/alerts/alerts.json`. All five +were confirmed to fire: + +| Aggregate rule | Trigger | Correlates on | Level | +|---|---|---|---| +| 112004 | 8x rule 112003 in 300s | `same_source_ip` | 10 | +| 112022 | 20x rule 112021 in 120s | `same_source_ip` | 8 | +| 112033 | 30x rule 112030 in 300s | `same_source_ip` | 10 | +| 112043 | 15x rule 112042 in 120s | `same_source_ip` | 10 | +| 112052 | 25x rule 112051 in 300s | `same_field oci.srcuser` | 10 | + +#### 2. Connectivity — against your tenancy + +Confirm credentials, policy and the delivery target without writing any alerts: + +```bash +/var/ossec/wodles/oci/oci-logs.py \ + --source objectstorage --bucket wazuh-oci-logs \ + --config-file /var/ossec/wodles/oci/config \ + --output stdout --max-records 5 --log-level DEBUG +``` + +A `NotAuthorizedOrNotFound` here means the IAM policy is missing or the bucket name +or compartment is wrong — not a bug in the collector. + +#### 3. End to end — generate a real event + +Trigger something the ruleset alerts on, then wait for Connector Hub to batch it +(allow a few minutes; up to 17 for a Functions-task pipeline): + +```bash +oci iam user create --name wazuh-oci-test --description "Wazuh integration test" +oci iam user delete --user-id --force +``` + +Then check for the alert: + +```bash +sudo grep -F '"integration":"oci"' /var/ossec/logs/alerts/alerts.json | tail -5 +sudo jq 'select(.rule.id | startswith("1120"))' /var/ossec/logs/alerts/alerts.json | tail -40 +``` + +Expected — a rule 112005 alert (level 10, *OCI IAM: identity object modified*) and a +rule 112006 alert (level 12) for the delete. + +In the Wazuh dashboard, filter on `data.integration: oci` or `rule.groups: oci`. + +--- + +### Service Coverage Reference + +The eleven services asked about, and where their logs actually come from. Confirm the +service-log column in your own tenancy — availability varies by region and Oracle adds +categories over time. + +| Service | OCI Audit (control plane) | Service log (data plane) | Rules | +|---|---|---|---| +| **Virtual Cloud Network (VCN)** | Yes — VCN, subnet, security list, NSG, route table changes | **Yes** — VCN Flow Logs, per subnet or VNIC | 112007, 112020-112022 | +| **Dynamic Routing Gateway (DRG)** | Yes — DRG and attachment create/update/delete, route distribution | No dedicated log. Traffic crossing a DRG attachment appears in the flow logs of the attached subnets | 112008, 112009 | +| **Block Storage** | Yes — volume, backup, clone, attach/detach | No | 112010, 112011 | +| **Object Storage** | Yes — bucket create/update/delete, policy changes | **Yes** — Read Access Events and Write Access Events, per bucket | 112012, 112013, 112030-112033 | +| **API Gateway** | Yes — gateway and deployment changes | **Yes** — Access and Execution logs, per deployment | 112040-112044 | +| **Oracle Autonomous Database** | Yes — provision, scale, stop, terminate, wallet and admin password | No OCI Logging category. Database-level auditing lives in the DB itself | 112014-112016 | +| **Autonomous Transaction Processing (ATP)** | Same as Autonomous Database — ATP is an ADB workload type | Same as above | 112014-112016 | +| **Oracle APEX** | Yes, indirectly — APEX runs inside an ADB, so ADB operations are audited | No. APEX activity is in `APEX_ACTIVITY_LOG` / `APEX_WORKSPACE_ACCESS_LOG` inside the database | 112014 | +| **Data Integration** | Yes — workspace, application, task changes | Check your tenancy | 112017, 112018 | +| **Email Delivery** | Yes — approved sender, suppression list, SMTP credential changes | **Yes** — outbound accepted / relayed / suppressed | 112050-112052 | +| **Integration Service (OIC)** | Yes — instance create, update, start, stop, delete | Check your tenancy. OIC also keeps its own activity stream inside the instance | 112017, 112018 | + +#### Getting database-level visibility for ADB, ATP and APEX + +Audit covers *who resized the database*, not *who queried the salary table*. For +in-database activity you need one of these, and neither is shipped here: + +**Oracle Data Safe** — the supported route. Register the ADB with Data Safe, enable +audit collection, then use a Connector Hub connector from the Data Safe audit log +group into the same bucket or stream this collector already reads. No extra Wazuh +configuration; the records arrive with `oci.log_type: audit`. + +**Scheduled unified-audit export** — if Data Safe is not an option, export from the +database on a schedule and tail the result. Run as a user with `SELECT` on the audit +view: + +```sql +SELECT JSON_OBJECT( + 'event_timestamp' VALUE EVENT_TIMESTAMP, + 'db_user' VALUE DBUSERNAME, + 'client_ip' VALUE CLIENT_HOST, + 'action' VALUE ACTION_NAME, + 'object' VALUE OBJECT_SCHEMA || '.' || OBJECT_NAME, + 'returncode' VALUE RETURN_CODE, + 'sql_text' VALUE SQL_TEXT) + FROM UNIFIED_AUDIT_TRAIL + WHERE EVENT_TIMESTAMP > SYSTIMESTAMP - INTERVAL '5' MINUTE; +``` + +For APEX specifically, `APEX_WORKSPACE_ACCESS_LOG` records workspace sign-ins and +`APEX_ACTIVITY_LOG` records page views. Write either to a file and collect it with a +`` block using `json`. + +--- + +### Troubleshooting + +| Symptom | Cause and fix | +|---|---| +| `Wazuh queue socket not found` | The manager is not running, or the collector is on an agent. Use `--output file` plus a `` block. | +| `NotAuthorizedOrNotFound` | IAM policy missing, or wrong compartment/bucket/stream. Verify with `oci os object list --bucket-name ` as the same user. | +| Collector runs clean, no events | The connector has not batched yet, or it is failing. Check **Connector Hub > your connector > Metrics**, and enable connector logs to see delivery errors. | +| Events reach Wazuh but no alerts fire | Confirm the JSON decoder matched: `wazuh-logtest` should show `json` as the decoder. Then check `oracle_cloud_oci_rules.xml` is in `/var/ossec/etc/rules/` with `wazuh` group ownership. | +| Duplicate events after editing the connector | Expected. Editing a connector's source or target resets it internally and it may re-read from an earlier offset. Create a new connector rather than editing a working one. | +| `Event of N bytes exceeds the 65535 byte analysisd limit` | A single OCI record was too large. The collector drops `oci.data` and forwards the normalised fields with `oci.truncated: true` rather than losing the event. Add a Connector Hub log-filter task to trim the payload at source. | +| Streaming collector reprocesses everything | `--group-name` changed, or the consumer group aged out. Consumer-group offsets are server-side; keep the group name stable. | +| Object Storage collector reprocesses everything | `--state-file` was deleted or is not writable. Default is `/var/ossec/var/run/oci-logs.state`. | +| Flow logs drown out everything else | Add a Connector Hub log-filter task dropping `data.action = "ACCEPT"`, or collect flow logs through a separate connector so you can tune them independently. | + +--- + +### Provenance and Maintenance + +* **Original source**: Design informed by the OCI Streaming collector contributed by + Felix Bocco in [wazuh/integrations#51](https://github.com/wazuh/integrations/pull/51), + which established the consumer-group approach. The collector here is new code, not a + modification of that one. Written against Oracle's + [Connector Hub](https://docs.oracle.com/en-us/iaas/Content/connector-hub/overview.htm) + and [Logging](https://docs.oracle.com/en-us/iaas/Content/Logging/home.htm) + documentation, and shaped by the guidance in the Wazuh community thread on + forwarding OCI logs. +* **Adapted by**: Tamir Suliman. +* **Relationship to the other OCI integrations**: this adds a third and removes + nothing. `Oracle_Cloud_Infrastructure_streaming/` (Jose Camargo, + [#55](https://github.com/wazuh/integrations/pull/55)) routes Streaming through + Logstash and Kafka and ships a dashboard. `Oracle-Integration-(OCI)/` (Felix Bocco, + [#51](https://github.com/wazuh/integrations/pull/51)) is Streaming-only and ships no + rules. This one uses the OCI SDK directly, supports both Connector Hub delivery + targets, and brings a ruleset. Its rules file is `oracle_cloud_oci_rules.xml` so it + cannot overwrite the others once installed. Which to consolidate, if any, is a call + for the repository maintainers. +* **What is new here**: the Object Storage source and the file/`` output; + instance-principal and resource-principal auth; record normalisation and a stable + field schema; the 35-rule `112000-112099` ruleset; offline replay for testing. +* **Tested versions**: Wazuh manager **4.14.7** (Ubuntu 22.04, aarch64, installed from + `packages.wazuh.com/4.x/apt`); Python 3.9-3.13; `oci` SDK 2.126+. Rules use only + 4.4+ syntax. +* **Maintainer**: community-maintained. Open an issue or PR against + [wazuh/integrations](https://github.com/wazuh/integrations). +* **Support boundary**: Provided as is, community-maintained. This is **not** a native + Wazuh module and is not covered by Wazuh support. + + *Verified on a running manager*: the ruleset loads (`wazuh-analysisd -t`); all 21 + bundled samples land on the expected rule and level under `wazuh-logtest`; both + ingestion paths produce alerts end to end — the analysisd socket (`location: oci`) + and a file tailed by ``; all five frequency rules fire, including + `same_source_ip` and `same_field` correlation. All 35 rules are exercised — 25 fire + directly from the samples, the other 10 are parents traversed on the way. + + *Not verified*: the live OCI Streaming and Object Storage code paths have not run + against a real tenancy. Record shapes vary by service and region, so validate in a + non-production compartment first and please report field-shape differences you hit. + +--- + +### Sources + +* [Overview of Connector Hub](https://docs.oracle.com/en-us/iaas/Content/connector-hub/overview.htm) +* [Connector Hub scenarios](https://docs.oracle.com/en-us/iaas/Content/connector-hub/scenarios.htm) +* [OCI Logging overview](https://docs.oracle.com/en-us/iaas/Content/Logging/home.htm) +* [OCI Logging service logs and categories](https://docs.oracle.com/en-us/iaas/Content/Logging/Concepts/service_logs.htm) +* [OCI Audit overview](https://docs.oracle.com/en-us/iaas/Content/Audit/home.htm) +* [VCN Flow Logs](https://docs.oracle.com/en-us/iaas/Content/Network/Concepts/vcn_flow_logs.htm) +* [OCI Streaming overview](https://docs.oracle.com/en-us/iaas/Content/Streaming/home.htm) +* [OCI SDK for Python](https://docs.oracle.com/en-us/iaas/tools/python/latest/) +* [Calling services from an instance (instance principals)](https://docs.oracle.com/en-us/iaas/Content/Identity/Tasks/callingservicesfrominstances.htm) +* [Oracle Data Safe activity auditing](https://docs.oracle.com/en-us/iaas/data-safe/doc/activity-auditing-overview.html) +* [Wazuh log data collection](https://documentation.wazuh.com/current/user-manual/capabilities/log-data-collection/index.html) +* [Wazuh custom rules](https://documentation.wazuh.com/current/user-manual/ruleset/rules/custom.html) diff --git a/integrations/oracle_cloud_oci/config.example b/integrations/oracle_cloud_oci/config.example new file mode 100644 index 0000000..8297491 --- /dev/null +++ b/integrations/oracle_cloud_oci/config.example @@ -0,0 +1,25 @@ +# OCI API-key credentials for the Wazuh collector. +# +# Install to /var/ossec/wodles/oci/config with 0600 permissions: +# sudo install -m 0600 -o root -g root config.example /var/ossec/wodles/oci/config +# +# Generate the key pair and read the fingerprint/OCIDs from the Console under +# Identity > Users > > API Keys. +# +# Prefer --auth instance_principal instead of this file when the collector runs +# on an OCI compute instance: it removes the private key from disk entirely. + +[DEFAULT] +user=ocid1.user.oc1..aaaaaaaaEXAMPLE +fingerprint=2f:31:ae:1a:5f:e4:a5:5b:6a:15:9c:8a:1c:7f:1d:53 +key_file=/var/ossec/wodles/oci/oci_api_key.pem +tenancy=ocid1.tenancy.oc1..aaaaaaaaEXAMPLE +region=us-ashburn-1 + +# Optional: only needed if you pass --profile READONLY. +# [READONLY] +# user=ocid1.user.oc1..aaaaaaaaEXAMPLE2 +# fingerprint=... +# key_file=/var/ossec/wodles/oci/oci_api_key_ro.pem +# tenancy=ocid1.tenancy.oc1..aaaaaaaaEXAMPLE +# region=us-ashburn-1 diff --git a/integrations/oracle_cloud_oci/oci-logs.py b/integrations/oracle_cloud_oci/oci-logs.py new file mode 100644 index 0000000..a932a1f --- /dev/null +++ b/integrations/oracle_cloud_oci/oci-logs.py @@ -0,0 +1,659 @@ +#!/var/ossec/framework/python/bin/python3 +"""Forward Oracle Cloud Infrastructure (OCI) logs into Wazuh. + +OCI has no native Wazuh wodle. The supported path is: + + OCI service -> Logging (+ Audit) -> Connector Hub -> Streaming + \\-> Object Storage + +This script is the last hop: it reads the log records Connector Hub delivered +to a stream or a bucket, normalises them, and hands them to Wazuh -- either +straight to analysisd's socket or as JSON lines on disk for a +block to tail. + +Sources: + streaming poll an OCI Streaming stream using a consumer group + objectstorage poll an Object Storage bucket, checkpointing by object name + local replay a file of OCI records (offline testing, no tenancy) + +Outputs: + analysisd datagram to the Wazuh queue socket, header "1:oci:" + file newline-delimited JSON for json + stdout print, for piping into wazuh-logtest +""" + +import argparse +import gzip +import json +import logging +import os +import sys +from base64 import b64decode +from datetime import datetime, timezone +from socket import AF_UNIX, SOCK_DGRAM, SOL_SOCKET, SO_SNDBUF, socket +from socket import error as socket_error + +try: + import oci +except ImportError: # only 'local' source works without the SDK + oci = None + +SOCKET_HEADER = '1:oci:' +# analysisd drops datagrams larger than this. +MAX_EVENT_SIZE = 65535 +DEFAULT_STATE_FILE = '/var/ossec/var/run/oci-logs.state' + +LOGGING_MSG_FORMAT = '%(asctime)s oci-logs: %(levelname)s: %(message)s' +LOGGING_DATE_FORMAT = '%Y/%m/%d %H:%M:%S' + +logger = logging.getLogger('oci-logs') + + +# -------------------------------------------------------------------------- +# Wazuh paths +# -------------------------------------------------------------------------- + +def find_wazuh_path() -> str: + """Walk up from this file to the Wazuh root, or fall back to /var/ossec.""" + parts = os.path.abspath(os.path.dirname(__file__)).split(os.sep) + for anchor in ('wodles', 'integrations'): + if anchor in parts: + return os.sep.join(parts[:parts.index(anchor)]) or os.sep + return '/var/ossec' + + +ANALYSISD = os.path.join(find_wazuh_path(), 'queue', 'sockets', 'queue') + + +# -------------------------------------------------------------------------- +# Record normalisation +# -------------------------------------------------------------------------- + +# Service-log types, longest prefix first. Audit records are detected by shape +# below because their types collide with these (both start com.oraclecloud.*). +SERVICE_LOG_TYPES = ( + ('com.oraclecloud.vcn.flowlogs', 'vcn_flow'), + ('com.oraclecloud.apigateway.access', 'apigateway_access'), + ('com.oraclecloud.apigateway.execution', 'apigateway_execution'), + ('com.oraclecloud.objectstorage', 'objectstorage'), + ('com.oraclecloud.emaildelivery', 'emaildelivery'), + ('com.oraclecloud.dataintegration', 'data_integration'), + ('com.oraclecloud.integration', 'integration_service'), + ('com.oraclecloud.loadbalancer', 'loadbalancer'), + ('com.oraclecloud.waf', 'waf'), + ('com.oraclecloud.functions', 'functions'), + ('com.oraclecloud.oke', 'oke'), + ('com.oraclecloud.database', 'database'), + ('com.oraclecloud.autonomousdatabase', 'database'), +) + +# Where to look for a value, in order, when hoisting normalised fields. +SEARCH_PATHS = ((), ('identity',), ('request',), ('response',)) + +FIELD_ALIASES = ( + ('srcip', ('sourceAddress', 'clientIp', 'clientIpAddress', 'sourceIp', + 'srcaddr', 'remoteAddress', 'senderIp', 'ipAddress')), + ('dstip', ('destinationAddress', 'dstaddr', 'targetAddress')), + ('srcport', ('sourcePort', 'srcport')), + ('dstport', ('destinationPort', 'dstport')), + ('srcuser', ('principalName', 'userName', 'sender', 'principalId')), + ('dstuser', ('recipient',)), + ('action', ('action', 'httpMethod', 'method', 'operation')), + ('status', ('status', 'responseStatus', 'statusCode', 'httpStatusCode')), + ('url', ('path', 'requestUri', 'requestPath', 'uri', 'url')), + ('protocol', ('protocolName', 'protocol')), +) + +# Envelope keys carrying resource context, mapped to friendlier names. OCI +# lowercases these inside logContent.oracle. +ORACLE_KEYS = ( + ('compartmentid', 'compartment_id'), + ('compartmentname', 'compartment_name'), + ('loggroupid', 'log_group_id'), + ('logid', 'log_id'), + ('tenantid', 'tenant_id'), + ('ingestedtime', 'ingested_time'), + ('subnetocid', 'subnet_id'), + ('vnicocid', 'vnic_id'), + ('vniccompartmentocid', 'vnic_compartment_id'), + ('instanceocid', 'instance_id'), +) + + +def _dig(obj, *keys): + """Fetch a nested key path, returning None instead of raising.""" + for key in keys: + if not isinstance(obj, dict): + return None + obj = obj.get(key) + return obj + + +def unwrap(record: dict) -> dict: + """Strip the Logging envelope. Object Storage delivery keeps records under + 'logContent'; Streaming delivery usually does not.""" + inner = record.get('logContent') + return inner if isinstance(inner, dict) else record + + +def classify(record: dict) -> tuple: + """Return (log_type, service) for an OCI log record.""" + data = record.get('data') + data = data if isinstance(data, dict) else {} + event_type = (record.get('type') or record.get('eventType') or '') + source = record.get('source') or '' + if source == '-': # flow logs use '-' as a placeholder source + source = '' + + segments = event_type.split('.') + + # Audit records always carry both an eventName and an identity block. + if 'eventName' in data and 'identity' in data: + # com.oraclecloud.. -> + return 'audit', source or (segments[2] if len(segments) > 3 else '') + + lowered = event_type.lower() + for prefix, log_type in SERVICE_LOG_TYPES: + if lowered.startswith(prefix): + return log_type, source or prefix.split('.')[2] + + return 'oci', source or (segments[2] if len(segments) > 2 else '') + + +def hoist(record: dict, log_type: str) -> dict: + """Lift a handful of values to top-level Wazuh field names so rules and the + alert schema can use srcip/srcuser/action/status directly.""" + data = record.get('data') + data = data if isinstance(data, dict) else {} + out = {} + + for field, aliases in FIELD_ALIASES: + for path in SEARCH_PATHS: + scope = _dig(data, *path) if path else data + if not isinstance(scope, dict): + continue + for alias in aliases: + value = scope.get(alias) + if value not in (None, '', [], {}): + out[field] = value if isinstance(value, str) else str(value) + break + if field in out: + break + + # For audit, the operation name is far more useful than the HTTP verb. + if log_type == 'audit' and data.get('eventName'): + out['action'] = data['eventName'] + + return out + + +def normalise(raw: dict) -> dict: + """Turn one OCI log record into the event Wazuh receives.""" + record = unwrap(raw) + log_type, service = classify(record) + oracle = record.get('oracle') + oracle = oracle if isinstance(oracle, dict) else {} + + oci_block = { + 'log_type': log_type, + 'service': service, + 'type': record.get('type') or record.get('eventType') or '', + 'time': record.get('time') or record.get('eventTime') or '', + 'id': record.get('id') or record.get('eventId') or '', + 'data': record.get('data'), + } + for source_key, dest_key in ORACLE_KEYS: + if oracle.get(source_key): + oci_block[dest_key] = oracle[source_key] + + # Audit carries compartment on the payload rather than the envelope. + data = record.get('data') + if isinstance(data, dict): + oci_block.setdefault('compartment_id', data.get('compartmentId') or '') + oci_block.setdefault('compartment_name', data.get('compartmentName') or '') + if data.get('resourceName'): + oci_block['resource_name'] = data['resourceName'] + if data.get('resourceId'): + oci_block['resource_id'] = data['resourceId'] + oci_block = {k: v for k, v in oci_block.items() if v not in (None, '')} + + # Emitted twice on purpose. At the top level these land on Wazuh's *static* + # decoder fields, which is what populates srcip/srcuser/status in the alert + # schema and what keys on. But analysisd matches static + # fields with strcmp () or OSMatch (), so neither supports + # the regex the rules need. The mirror under oci.* is dynamic, and + # on a dynamic name gets full OS_Regex. + hoisted = hoist(record, log_type) + oci_block.update(hoisted) + # Rules gate on oci.status with negate="yes", which does not match when the + # field is absent. Always emitting it keeps records that carry no status + # from falling out of the ruleset. Only the mirror gets the placeholder -- + # the top-level static field stays empty so the alert schema shows no + # status rather than a fabricated one. + oci_block.setdefault('status', 'unknown') + + event = {'integration': 'oci', 'oci': oci_block} + event.update(hoisted) + return event + + +def parse_payload(payload: bytes): + """Yield records from a Connector Hub payload. + + Handles gzip or plain, and either a JSON array/object or newline-delimited + JSON -- Connector Hub has used both shapes depending on target and version. + """ + if payload[:2] == b'\x1f\x8b': + payload = gzip.decompress(payload) + text = payload.decode('utf-8', errors='replace').strip() + if not text: + return + + try: + parsed = json.loads(text) + except json.JSONDecodeError: + pass + else: + for item in (parsed if isinstance(parsed, list) else [parsed]): + if isinstance(item, dict): + yield item + return + + for line_no, line in enumerate(text.splitlines(), 1): + line = line.strip() + if not line: + continue + try: + item = json.loads(line) + except json.JSONDecodeError as err: + logger.warning('Skipping unparseable line %d: %s', line_no, err) + continue + if isinstance(item, dict): + yield item + + +# -------------------------------------------------------------------------- +# Outputs +# -------------------------------------------------------------------------- + +class Output: + """Where normalised events go.""" + + def __init__(self, mode: str, path: str = None): + self.mode = mode + self.path = path + self._sock = None + self._fh = None + self.sent = 0 + + if mode == 'file': + if not path: + raise ValueError('--output file requires --path') + directory = os.path.dirname(os.path.abspath(path)) + if directory: + os.makedirs(directory, exist_ok=True) + self._fh = open(path, 'a', encoding='utf-8') + + def _connect(self): + if self._sock is not None: + return + if not os.path.exists(ANALYSISD): + logger.error('Wazuh queue socket not found at %s. Is the manager ' + 'running, or did you mean --output file?', ANALYSISD) + sys.exit(1) + sock = socket(AF_UNIX, SOCK_DGRAM) + sock.setsockopt(SOL_SOCKET, SO_SNDBUF, MAX_EVENT_SIZE) + sock.connect(ANALYSISD) + self._sock = sock + + def send(self, event: dict): + body = json.dumps(event) + if self.mode == 'analysisd': + body = self._fit(event, body) + self._connect() + try: + self._sock.send(f'{SOCKET_HEADER}{body}'.encode(errors='replace')) + except socket_error as err: + logger.error('Socket error sending to analysisd: %s', err) + self._sock.close() + self._sock = None + raise + elif self.mode == 'file': + self._fh.write(body + '\n') + else: + print(body) + self.sent += 1 + + def _fit(self, event: dict, body: str) -> str: + """Drop the raw payload rather than lose an oversized event entirely.""" + if len(body) + len(SOCKET_HEADER) <= MAX_EVENT_SIZE: + return body + logger.warning('Event of %d bytes exceeds the %d byte analysisd limit; ' + 'dropping oci.data', len(body), MAX_EVENT_SIZE) + trimmed = dict(event) + trimmed['oci'] = {k: v for k, v in event['oci'].items() if k != 'data'} + trimmed['oci']['truncated'] = True + return json.dumps(trimmed) + + def close(self): + if self._sock is not None: + self._sock.close() + if self._fh is not None: + self._fh.close() + + +# -------------------------------------------------------------------------- +# State +# -------------------------------------------------------------------------- + +def load_state(path: str) -> dict: + try: + with open(path, encoding='utf-8') as handle: + return json.load(handle) + except FileNotFoundError: + return {} + except (OSError, json.JSONDecodeError) as err: + logger.warning('Ignoring unreadable state file %s: %s', path, err) + return {} + + +def save_state(path: str, state: dict): + directory = os.path.dirname(os.path.abspath(path)) + if directory: + os.makedirs(directory, exist_ok=True) + tmp = f'{path}.tmp' + with open(tmp, 'w', encoding='utf-8') as handle: + json.dump(state, handle) + os.replace(tmp, path) + + +# -------------------------------------------------------------------------- +# Authentication +# -------------------------------------------------------------------------- + +def build_auth(args): + """Return (config, signer). signer is None for plain API-key auth.""" + if oci is None: + logger.error("The 'oci' Python SDK is not installed. " + 'Run: pip install -r requirements.txt') + sys.exit(1) + + if args.auth == 'instance_principal': + signer = oci.auth.signers.InstancePrincipalsSecurityTokenSigner() + return {'region': args.region or signer.region}, signer + + if args.auth == 'resource_principal': + signer = oci.auth.signers.get_resource_principals_signer() + return {'region': args.region or signer.region}, signer + + config = oci.config.from_file(args.config_file, args.profile) + if args.region: + config['region'] = args.region + oci.config.validate_config(config) + return config, None + + +def make_client(client_class, config, signer, **kwargs): + if signer is not None: + return client_class(config, signer=signer, **kwargs) + return client_class(config, **kwargs) + + +# -------------------------------------------------------------------------- +# Sources +# -------------------------------------------------------------------------- + +def run_streaming(args, output, config, signer): + """Consume a stream via a consumer group. OCI tracks the offset server-side, + so restarts resume where the previous run stopped.""" + endpoint = args.stream_endpoint + if not endpoint: + admin = make_client(oci.streaming.StreamAdminClient, config, signer) + endpoint = admin.get_stream(args.stream_id).data.messages_endpoint + logger.debug('Resolved messages endpoint: %s', endpoint) + + client = make_client(oci.streaming.StreamClient, config, signer, + service_endpoint=endpoint) + + cursor = client.create_group_cursor( + args.stream_id, + oci.streaming.models.CreateGroupCursorDetails( + group_name=args.group_name, + instance_name=args.instance_name, + type=getattr(oci.streaming.models.CreateGroupCursorDetails, + f'TYPE_{args.cursor_type.upper()}'), + commit_on_get=True, + ), + ).data.value + logger.info("Consuming stream %s as group '%s' instance '%s'", + args.stream_id, args.group_name, args.instance_name) + + empty_polls = 0 + while True: + response = client.get_messages(args.stream_id, cursor, limit=args.limit) + messages = response.data or [] + + if not messages: + empty_polls += 1 + if empty_polls >= args.empty_polls: + logger.info('Stream drained after %d empty polls', empty_polls) + break + else: + empty_polls = 0 + + for message in messages: + try: + payload = b64decode(message.value) + except Exception as err: + logger.warning('Skipping message with undecodable value: %s', err) + continue + for record in parse_payload(payload): + output.send(normalise(record)) + + cursor = response.headers['opc-next-cursor'] + if args.max_records and output.sent >= args.max_records: + logger.info('Reached --max-records (%d)', args.max_records) + break + + +def run_objectstorage(args, output, config, signer): + """Walk the bucket in name order, checkpointing the last object read. + + Connector Hub writes objects under a time-ordered key prefix, so lexical + order is chronological and a name checkpoint is enough to resume. + """ + client = make_client(oci.object_storage.ObjectStorageClient, config, signer) + namespace = args.namespace or client.get_namespace().data + logger.debug('Object Storage namespace: %s', namespace) + + state = load_state(args.state_file) + key = f'objectstorage:{namespace}/{args.bucket}/{args.prefix or ""}' + last_object = state.get(key, {}).get('last_object') + if last_object: + logger.info('Resuming after object %s', last_object) + + start = last_object + processed = 0 + while True: + listing = client.list_objects( + namespace, args.bucket, + prefix=args.prefix or None, + start=start, + limit=min(args.limit, 1000), + fields='name,timeCreated,size', + ).data + + summaries = [obj for obj in listing.objects if obj.name != last_object] + for summary in summaries: + if args.start_time and summary.time_created and \ + summary.time_created < args.start_time: + logger.debug('Skipping %s (older than --start-time)', summary.name) + last_object = summary.name + continue + + logger.debug('Reading %s (%s bytes)', summary.name, summary.size) + payload = client.get_object(namespace, args.bucket, summary.name).data.content + for record in parse_payload(payload): + output.send(normalise(record)) + + last_object = summary.name + processed += 1 + state[key] = { + 'last_object': last_object, + 'updated': datetime.now(timezone.utc).isoformat(), + } + save_state(args.state_file, state) + + if args.delete_after_read: + client.delete_object(namespace, args.bucket, summary.name) + logger.debug('Deleted %s', summary.name) + + if args.max_records and output.sent >= args.max_records: + logger.info('Reached --max-records (%d)', args.max_records) + return + + if not listing.next_start_with: + break + start = listing.next_start_with + + logger.info('Processed %d object(s)', processed) + + +def run_local(args, output, _config=None, _signer=None): + """Replay records from a local file. No tenancy or SDK required.""" + with open(args.input, 'rb') as handle: + payload = handle.read() + for record in parse_payload(payload): + output.send(normalise(record)) + if args.max_records and output.sent >= args.max_records: + break + + +# -------------------------------------------------------------------------- +# CLI +# -------------------------------------------------------------------------- + +def valid_datetime(value: str) -> datetime: + for mask in ('%Y-%m-%dT%H:%M:%S', '%Y-%m-%d'): + try: + return datetime.strptime(value, mask).replace(tzinfo=timezone.utc) + except ValueError: + continue + raise argparse.ArgumentTypeError( + f"'{value}' is not YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS") + + +def get_arguments(argv=None): + parser = argparse.ArgumentParser( + description='Forward OCI logs collected by Connector Hub into Wazuh.', + formatter_class=argparse.RawTextHelpFormatter) + + parser.add_argument('-s', '--source', required=True, + choices=['streaming', 'objectstorage', 'local'], + help='Where Connector Hub delivered the logs.') + + auth = parser.add_argument_group('authentication') + auth.add_argument('--auth', default='config', + choices=['config', 'instance_principal', 'resource_principal'], + help='config: API key file (default). instance_principal:\n' + 'no keys, for an OCI compute instance in a dynamic group.') + auth.add_argument('-c', '--config-file', default='~/.oci/config', + help='OCI config file (default: ~/.oci/config).') + auth.add_argument('-p', '--profile', default='DEFAULT', + help='Profile within the config file (default: DEFAULT).') + auth.add_argument('--region', help='Override the region from the config.') + + stream = parser.add_argument_group('streaming source') + stream.add_argument('--stream-id', help='OCID of the stream.') + stream.add_argument('--stream-endpoint', + help='Messages endpoint. Looked up automatically if omitted.') + stream.add_argument('--group-name', default='wazuh', + help='Consumer group. Change it to re-read from the start.') + stream.add_argument('--instance-name', default='wazuh-1', + help='Consumer instance, unique per collector process.') + stream.add_argument('--cursor-type', default='TRIM_HORIZON', + choices=['TRIM_HORIZON', 'LATEST'], + help='Where a brand new consumer group starts reading.') + stream.add_argument('--empty-polls', type=int, default=2, + help='Consecutive empty polls before exiting (default: 2).') + + store = parser.add_argument_group('objectstorage source') + store.add_argument('--namespace', help='Object Storage namespace (auto-detected).') + store.add_argument('--bucket', help='Bucket receiving the Connector Hub output.') + store.add_argument('--prefix', help='Object name prefix to restrict the listing.') + store.add_argument('--start-time', type=valid_datetime, + help='Ignore objects created before this UTC time.') + store.add_argument('--delete-after-read', action='store_true', + help='Delete each object once forwarded.') + + local = parser.add_argument_group('local source') + local.add_argument('-i', '--input', help='File of OCI records to replay.') + + out = parser.add_argument_group('output') + out.add_argument('-o', '--output', default='analysisd', + choices=['analysisd', 'file', 'stdout'], + help='analysisd: Wazuh queue socket (default).\n' + 'file: JSON lines for a block.') + out.add_argument('--path', help='Destination file when --output file.') + + parser.add_argument('--state-file', default=DEFAULT_STATE_FILE, + help=f'Checkpoint file (default: {DEFAULT_STATE_FILE}).') + parser.add_argument('--limit', type=int, default=250, + help='Records or objects fetched per request (default: 250).') + parser.add_argument('--max-records', type=int, default=0, + help='Stop after this many events. 0 means no limit.') + parser.add_argument('-l', '--log-level', default='INFO', + choices=['DEBUG', 'INFO', 'WARNING', 'ERROR']) + + args = parser.parse_args(argv) + args.config_file = os.path.expanduser(args.config_file) + + required = { + 'streaming': [('stream_id', '--stream-id')], + 'objectstorage': [('bucket', '--bucket')], + 'local': [('input', '--input')], + }[args.source] + missing = [flag for attr, flag in required if not getattr(args, attr)] + if missing: + parser.error(f"--source {args.source} requires {', '.join(missing)}") + + return args + + +def main(argv=None) -> int: + args = get_arguments(argv) + logging.basicConfig(format=LOGGING_MSG_FORMAT, datefmt=LOGGING_DATE_FORMAT, + stream=sys.stderr) + logger.setLevel(args.log_level) + + try: + output = Output(args.output, args.path) + except ValueError as err: + logger.error('%s', err) + return 1 + + config, signer = (None, None) + if args.source != 'local': + config, signer = build_auth(args) + + runner = {'streaming': run_streaming, + 'objectstorage': run_objectstorage, + 'local': run_local}[args.source] + + try: + runner(args, output, config, signer) + except KeyboardInterrupt: + logger.info('Interrupted') + except Exception as err: + logger.error('Collection failed: %s', err) + logger.debug('Traceback', exc_info=True) + return 1 + finally: + logger.info('Forwarded %d event(s) via %s', output.sent, args.output) + output.close() + + return 0 + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/integrations/oracle_cloud_oci/requirements.txt b/integrations/oracle_cloud_oci/requirements.txt new file mode 100644 index 0000000..524d034 --- /dev/null +++ b/integrations/oracle_cloud_oci/requirements.txt @@ -0,0 +1,3 @@ +# Only needed for --source streaming and --source objectstorage. +# --source local (offline replay/testing) runs on the standard library alone. +oci>=2.126.0 diff --git a/integrations/oracle_cloud_oci/ruleset/rules/oracle_cloud_oci_rules.xml b/integrations/oracle_cloud_oci/ruleset/rules/oracle_cloud_oci_rules.xml new file mode 100644 index 0000000..c1b79dd --- /dev/null +++ b/integrations/oracle_cloud_oci/ruleset/rules/oracle_cloud_oci_rules.xml @@ -0,0 +1,374 @@ + + + + + + json + ^oci$ + Oracle Cloud Infrastructure log received. + no_full_log + + + + + + 112000 + ^audit$ + OCI Audit: $(action) on $(oci.resource_name) by $(srcuser). + + T1078.004 + + no_full_log + + + + 112001 + ^4\d\d$|^5\d\d$ + OCI Audit: API call $(action) failed with status $(status). + no_full_log + + + + + 112002 + ^401$|^403$ + OCI Audit: $(srcuser) was denied permission to run $(action). + + T1078 + + no_full_log + + + + 112003 + + OCI Audit: repeated authorization failures from $(srcip). Possible privilege probing. + + T1078 + T1580 + + no_full_log + + + + + + 112001 + ^4\d\d$|^5\d\d$ + OCI Audit: API call succeeded. + no_full_log + + + + + + 112019 + ^identityControlPlane$ + ^Create|^Update|^Delete|^Add|^Remove + OCI IAM: identity object modified - $(action) on $(oci.resource_name) by $(srcuser). + + T1098 + + no_full_log + + + + 112005 + Policy$|^DeleteUser$|^DeleteGroup$ + OCI IAM: policy or principal deleted - $(action) on $(oci.resource_name) by $(srcuser). + + T1098 + T1531 + + no_full_log + + + + + + 112019 + ^virtualNetwork$ + SecurityList$|NetworkSecurityGroup$|SecurityRules$|RouteTable$ + OCI VCN: network filtering changed - $(action) on $(oci.resource_name) by $(srcuser). + + T1562.007 + + no_full_log + + + + 112019 + ^virtualNetwork$ + Drg + OCI DRG: $(action) on $(oci.resource_name) by $(srcuser). + no_full_log + + + + 112008 + ^Delete + OCI DRG: gateway or attachment deleted - $(oci.resource_name) by $(srcuser). Hybrid connectivity may be down. + + T1531 + + no_full_log + + + + + + 112019 + ^blockstorage$ + OCI Block Storage: $(action) on $(oci.resource_name) by $(srcuser). + no_full_log + + + + 112010 + ^Delete + OCI Block Storage: volume or backup deleted - $(oci.resource_name) by $(srcuser). + + T1485 + + no_full_log + + + + + + 112019 + ^objectstorage$ + ^DeleteBucket$ + OCI Object Storage: bucket $(oci.resource_name) deleted by $(srcuser). + + T1485 + + no_full_log + + + + 112019 + ^objectstorage$ + ^CreateBucket$|^UpdateBucket$ + ObjectRead + OCI Object Storage: bucket $(oci.resource_name) exposed with public read access by $(srcuser). + + T1530 + + no_full_log + + + + + + 112019 + ^databaseservice$ + OCI Database: $(action) on $(oci.resource_name) by $(srcuser). + no_full_log + + + + 112014 + ^Stop|^Delete|^Terminate|^Restore + OCI Autonomous Database/ATP: availability-affecting operation $(action) on $(oci.resource_name) by $(srcuser). + + T1489 + + no_full_log + + + + 112014 + Wallet$|AdminPassword$|^UpdateAutonomousDatabaseRegionalWallet$ + OCI Autonomous Database/ATP: database credential changed - $(action) on $(oci.resource_name) by $(srcuser). + + T1098 + + no_full_log + + + + + + 112019 + ^dataintegration$|^integration$ + OCI Integration: $(action) on $(oci.resource_name) by $(srcuser). + no_full_log + + + + 112017 + ^Delete|^Stop + OCI Integration: instance or workspace removed - $(action) on $(oci.resource_name) by $(srcuser). + + T1489 + + no_full_log + + + + + + 112000 + ^vcn_flow$ + OCI VCN flow log record. + no_full_log + + + + 112020 + ^REJECT$ + OCI VCN: traffic rejected from $(srcip) to $(dstip):$(dstport). + no_full_log + + + + 112021 + + OCI VCN: high volume of rejected connections from $(srcip). Possible port scan. + + T1046 + + no_full_log + + + + + + 112000 + ^objectstorage$ + OCI Object Storage: $(action) on $(oci.data.bucketName)/$(oci.data.objectName). + no_full_log + + + + + 112030 + ^DELETE$ + ^2\d\d$ + OCI Object Storage: object $(oci.data.objectName) deleted from bucket $(oci.data.bucketName). + + T1485 + + no_full_log + + + + 112030 + ^401$|^403$ + OCI Object Storage: access denied on $(oci.data.bucketName)/$(oci.data.objectName) from $(srcip). + + T1530 + + no_full_log + + + + 112030 + + ^GET$ + OCI Object Storage: unusually high object read volume from $(srcip). Possible data staging. + + T1530 + T1567.002 + + no_full_log + + + + + + 112000 + ^apigateway_access$|^apigateway_execution$ + OCI API Gateway: $(action) $(url) returned $(status). + no_full_log + + + + 112040 + ^4\d\d$ + OCI API Gateway: client error $(status) on $(action) $(url) from $(srcip). + no_full_log + + + + 112041 + ^401$|^403$ + OCI API Gateway: authentication or authorization failure on $(url) from $(srcip). + + T1190 + + no_full_log + + + + 112042 + + OCI API Gateway: repeated authentication failures from $(srcip). Possible credential brute force. + + T1110 + + no_full_log + + + + 112040 + ^5\d\d$ + OCI API Gateway: backend error $(status) on $(action) $(url). + no_full_log + + + + + + 112000 + ^emaildelivery$ + OCI Email Delivery: message from $(srcuser) to $(dstuser) - $(status). + no_full_log + + + + 112050 + SUPPRESSED|BOUNCE|FAIL|REJECT + OCI Email Delivery: message to $(dstuser) was not delivered - $(status). + no_full_log + + + + 112051 + oci.srcuser + OCI Email Delivery: high bounce or suppression rate for sender $(srcuser). Possible compromised sender or list abuse. + + T1566 + + no_full_log + + + diff --git a/integrations/oracle_cloud_oci/sample_logs.txt b/integrations/oracle_cloud_oci/sample_logs.txt new file mode 100644 index 0000000..1b4485c --- /dev/null +++ b/integrations/oracle_cloud_oci/sample_logs.txt @@ -0,0 +1,21 @@ +{"datetime":1751356800000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.identityControlPlane.CreateUser","source":"identityControlPlane","id":"a1b2c3d4-0000-4000-8000-000000000001","time":"2026-07-01T08:00:00.000Z","oracle":{"compartmentid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"_Audit","ingestedtime":"2026-07-01T08:00:12.345Z"},"data":{"eventGroupingId":"csid-0001","eventName":"CreateUser","compartmentId":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","compartmentName":"acme","resourceName":"svc-backup","resourceId":"ocid1.user.oc1..aaaaaaaasvcbackup","availabilityDomain":"","identity":{"principalName":"admin@acme.com","principalId":"ocid1.user.oc1..aaaaaaaaadminexample","authType":"natv","callerName":null,"callerId":null,"tenantId":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","ipAddress":"203.0.113.24","credentials":"ocid1.credential.oc1..aaaaaaaa","userAgent":"Oracle-JavaSDK/3.44.0"},"request":{"id":"req-0001","path":"/20160918/users","action":"POST","parameters":{},"headers":{}},"response":{"status":"200","responseTime":"2026-07-01T08:00:00.400Z","headers":{},"payload":{},"message":null},"stateChange":{"previous":null,"current":{"name":"svc-backup","lifecycleState":"ACTIVE"}},"additionalDetails":{}}}} +{"datetime":1751356860000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.objectstorage.DeleteBucket","source":"objectstorage","id":"a1b2c3d4-0000-4000-8000-000000000002","time":"2026-07-01T08:01:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"_Audit"},"data":{"eventName":"DeleteBucket","compartmentId":"ocid1.compartment.oc1..aaaaaaaaprod","compartmentName":"prod","resourceName":"finance-archive","resourceId":"ocid1.bucket.oc1..aaaaaaaafinance","identity":{"principalName":"contractor@acme.com","principalId":"ocid1.user.oc1..aaaaaaaacontractor","authType":"natv","tenantId":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","ipAddress":"198.51.100.77","userAgent":"OracleCLI/3.45.0"},"request":{"id":"req-0002","path":"/n/acmens/b/finance-archive","action":"DELETE","parameters":{},"headers":{}},"response":{"status":"204","responseTime":"2026-07-01T08:01:00.900Z","message":null}}}} +{"datetime":1751356920000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.identityControlPlane.CreateUser","source":"identityControlPlane","id":"a1b2c3d4-0000-4000-8000-000000000003","time":"2026-07-01T08:02:00.000Z","oracle":{"compartmentid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"_Audit"},"data":{"eventName":"CreateUser","compartmentId":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","compartmentName":"acme","resourceName":"mallory","identity":{"principalName":"contractor@acme.com","principalId":"ocid1.user.oc1..aaaaaaaacontractor","authType":"natv","tenantId":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","ipAddress":"198.51.100.77","userAgent":"OracleCLI/3.45.0"},"request":{"id":"req-0003","path":"/20160918/users","action":"POST"},"response":{"status":"403","responseTime":"2026-07-01T08:02:00.300Z","message":"Authorization failed or requested resource not found."}}}} +{"datetime":1751356980000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.virtualNetwork.UpdateSecurityList","source":"virtualNetwork","id":"a1b2c3d4-0000-4000-8000-000000000004","time":"2026-07-01T08:03:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"_Audit"},"data":{"eventName":"UpdateSecurityList","compartmentId":"ocid1.compartment.oc1..aaaaaaaaprod","compartmentName":"prod","resourceName":"prod-vcn-seclist","resourceId":"ocid1.securitylist.oc1.iad.aaaaaaaaseclist","identity":{"principalName":"netops@acme.com","principalId":"ocid1.user.oc1..aaaaaaaanetops","authType":"natv","tenantId":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","ipAddress":"203.0.113.90","userAgent":"Oracle-PythonSDK/2.140.0"},"request":{"id":"req-0004","path":"/20160918/securityLists/ocid1.securitylist.oc1.iad.aaaaaaaaseclist","action":"PUT"},"response":{"status":"200","responseTime":"2026-07-01T08:03:00.700Z"},"stateChange":{"current":{"ingressSecurityRules":[{"source":"0.0.0.0/0","protocol":"6","tcpOptions":{"destinationPortRange":{"min":22,"max":22}}}]}}}}} +{"datetime":1751357040000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.blockstorage.DeleteVolume","source":"blockstorage","id":"a1b2c3d4-0000-4000-8000-000000000005","time":"2026-07-01T08:04:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"_Audit"},"data":{"eventName":"DeleteVolume","compartmentId":"ocid1.compartment.oc1..aaaaaaaaprod","compartmentName":"prod","resourceName":"db-data-vol-01","resourceId":"ocid1.volume.oc1.iad.aaaaaaaadbdata","identity":{"principalName":"contractor@acme.com","principalId":"ocid1.user.oc1..aaaaaaaacontractor","authType":"natv","tenantId":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","ipAddress":"198.51.100.77","userAgent":"OracleCLI/3.45.0"},"request":{"id":"req-0005","path":"/20160918/volumes/ocid1.volume.oc1.iad.aaaaaaaadbdata","action":"DELETE"},"response":{"status":"204","responseTime":"2026-07-01T08:04:01.100Z"}}}} +{"datetime":1751357100000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.databaseservice.autonomous.database.StopAutonomousDatabase","source":"databaseservice","id":"a1b2c3d4-0000-4000-8000-000000000006","time":"2026-07-01T08:05:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"_Audit"},"data":{"eventName":"StopAutonomousDatabase","compartmentId":"ocid1.compartment.oc1..aaaaaaaaprod","compartmentName":"prod","resourceName":"atp-prod-01","resourceId":"ocid1.autonomousdatabase.oc1.iad.aaaaaaaaatpprod","identity":{"principalName":"contractor@acme.com","principalId":"ocid1.user.oc1..aaaaaaaacontractor","authType":"natv","tenantId":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","ipAddress":"198.51.100.77","userAgent":"OracleCLI/3.45.0"},"request":{"id":"req-0006","path":"/20160918/autonomousDatabases/ocid1.autonomousdatabase.oc1.iad.aaaaaaaaatpprod/actions/stop","action":"POST"},"response":{"status":"200","responseTime":"2026-07-01T08:05:02.400Z"}}}} +{"datetime":1751357160000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.vcn.flowlogs.DataEvent","source":"-","id":"a1b2c3d4-0000-4000-8000-000000000007","time":"2026-07-01T08:06:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"ocid1.loggroup.oc1.iad.aaaaaaaanetwork","logid":"ocid1.log.oc1.iad.aaaaaaaaflowlog","subnetocid":"ocid1.subnet.oc1.iad.aaaaaaaaprivate","vnicocid":"ocid1.vnic.oc1.iad.aaaaaaaavnic01","vniccompartmentocid":"ocid1.compartment.oc1..aaaaaaaaprod"},"data":{"version":2,"action":"REJECT","status":"OK","sourceAddress":"198.51.100.201","destinationAddress":"10.0.2.15","sourcePort":51422,"destinationPort":22,"protocol":6,"protocolName":"TCP","packets":1,"bytesOut":40,"startTime":1751357100000,"endTime":1751357160000,"flowid":"f0e1d2c3","targetSystem":"SERVICE"}}} +{"datetime":1751357220000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.vcn.flowlogs.DataEvent","source":"-","id":"a1b2c3d4-0000-4000-8000-000000000008","time":"2026-07-01T08:07:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"ocid1.loggroup.oc1.iad.aaaaaaaanetwork","logid":"ocid1.log.oc1.iad.aaaaaaaaflowlog","subnetocid":"ocid1.subnet.oc1.iad.aaaaaaaaprivate","vnicocid":"ocid1.vnic.oc1.iad.aaaaaaaavnic01"},"data":{"version":2,"action":"ACCEPT","status":"OK","sourceAddress":"10.0.1.9","destinationAddress":"10.0.2.15","sourcePort":44310,"destinationPort":1521,"protocol":6,"protocolName":"TCP","packets":18,"bytesOut":4096,"startTime":1751357160000,"endTime":1751357220000,"flowid":"a9b8c7d6","targetSystem":"SERVICE"}}} +{"datetime":1751357280000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.objectstorage.getobject","source":"objectstorage","id":"a1b2c3d4-0000-4000-8000-000000000009","time":"2026-07-01T08:08:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"ocid1.loggroup.oc1.iad.aaaaaaaastorage","logid":"ocid1.log.oc1.iad.aaaaaaaaosread"},"data":{"message":"GetObject succeeded","bucketName":"finance-archive","objectName":"2026/q2/payroll.csv","namespace":"acmens","principalId":"ocid1.user.oc1..aaaaaaaacontractor","clientIpAddress":"198.51.100.77","opcRequestId":"req-0009","status":"200","action":"GET","eTag":"1a2b3c4d","userAgent":"OracleCLI/3.45.0"}}} +{"datetime":1751357340000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.objectstorage.deleteobject","source":"objectstorage","id":"a1b2c3d4-0000-4000-8000-00000000000a","time":"2026-07-01T08:09:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"ocid1.loggroup.oc1.iad.aaaaaaaastorage","logid":"ocid1.log.oc1.iad.aaaaaaaaoswrite"},"data":{"message":"DeleteObject succeeded","bucketName":"finance-archive","objectName":"2026/q2/payroll.csv","namespace":"acmens","principalId":"ocid1.user.oc1..aaaaaaaacontractor","clientIpAddress":"198.51.100.77","opcRequestId":"req-000a","status":"204","action":"DELETE","userAgent":"OracleCLI/3.45.0"}}} +{"datetime":1751357400000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.apigateway.access","source":"apigateway","id":"a1b2c3d4-0000-4000-8000-00000000000b","time":"2026-07-01T08:10:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"ocid1.loggroup.oc1.iad.aaaaaaaagateway","logid":"ocid1.log.oc1.iad.aaaaaaaagwaccess"},"data":{"clientIp":"203.0.113.55","httpMethod":"POST","path":"/v1/orders","status":401,"responseTimeInMs":12,"requestId":"req-000b","userAgent":"curl/8.6.0","deploymentId":"ocid1.apideployment.oc1.iad.aaaaaaaaorders","gatewayId":"ocid1.apigateway.oc1.iad.aaaaaaaagw01","bytesOut":92}}} +{"datetime":1751357460000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.apigateway.access","source":"apigateway","id":"a1b2c3d4-0000-4000-8000-00000000000c","time":"2026-07-01T08:11:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"ocid1.loggroup.oc1.iad.aaaaaaaagateway","logid":"ocid1.log.oc1.iad.aaaaaaaagwaccess"},"data":{"clientIp":"203.0.113.55","httpMethod":"GET","path":"/v1/orders/9931","status":500,"responseTimeInMs":3021,"requestId":"req-000c","userAgent":"curl/8.6.0","deploymentId":"ocid1.apideployment.oc1.iad.aaaaaaaaorders","gatewayId":"ocid1.apigateway.oc1.iad.aaaaaaaagw01","bytesOut":140}}} +{"datetime":1751357520000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.emaildelivery.outboundsuppressed","source":"emaildelivery","id":"a1b2c3d4-0000-4000-8000-00000000000d","time":"2026-07-01T08:12:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"ocid1.loggroup.oc1.iad.aaaaaaaaemail","logid":"ocid1.log.oc1.iad.aaaaaaaaemailout"},"data":{"messageId":"msg-000d","sender":"noreply@acme.com","recipient":"customer@example.net","status":"SUPPRESSED","action":"SUPPRESSED","reason":"HARDBOUNCE","senderIp":"203.0.113.10","subject":"Invoice"}}} +{"datetime":1751357580000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.emaildelivery.outboundaccepted","source":"emaildelivery","id":"a1b2c3d4-0000-4000-8000-00000000000e","time":"2026-07-01T08:13:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"ocid1.loggroup.oc1.iad.aaaaaaaaemail","logid":"ocid1.log.oc1.iad.aaaaaaaaemailout"},"data":{"messageId":"msg-000e","sender":"noreply@acme.com","recipient":"customer@example.org","status":"ACCEPTED","action":"ACCEPTED","senderIp":"203.0.113.10","subject":"Welcome"}}} +{"datetime":1751357640000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.virtualNetwork.DeleteDrgAttachment","source":"virtualNetwork","id":"a1b2c3d4-0000-4000-8000-00000000000f","time":"2026-07-01T08:14:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"_Audit"},"data":{"eventName":"DeleteDrgAttachment","compartmentId":"ocid1.compartment.oc1..aaaaaaaaprod","compartmentName":"prod","resourceName":"prod-drg-attach","resourceId":"ocid1.drgattachment.oc1.iad.aaaaaaaaattach","identity":{"principalName":"netops@acme.com","principalId":"ocid1.user.oc1..aaaaaaaanetops","authType":"natv","tenantId":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","ipAddress":"203.0.113.90","userAgent":"Oracle-PythonSDK/2.140.0"},"request":{"id":"req-000f","path":"/20160918/drgAttachments/ocid1.drgattachment.oc1.iad.aaaaaaaaattach","action":"DELETE"},"response":{"status":"204","responseTime":"2026-07-01T08:14:01.000Z"}}}} +{"datetime":1751357700000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.integration.UpdateIntegrationInstance","source":"integration","id":"a1b2c3d4-0000-4000-8000-000000000010","time":"2026-07-01T08:15:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"_Audit"},"data":{"eventName":"UpdateIntegrationInstance","compartmentId":"ocid1.compartment.oc1..aaaaaaaaprod","compartmentName":"prod","resourceName":"oic-prod","resourceId":"ocid1.integrationinstance.oc1.iad.aaaaaaaaoicprod","identity":{"principalName":"appops@acme.com","principalId":"ocid1.user.oc1..aaaaaaaaappops","authType":"natv","tenantId":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","ipAddress":"203.0.113.31","userAgent":"Mozilla/5.0"},"request":{"id":"req-0010","path":"/20190131/integrationInstances/ocid1.integrationinstance.oc1.iad.aaaaaaaaoicprod","action":"PUT"},"response":{"status":"200","responseTime":"2026-07-01T08:15:03.200Z"}}}} +{"datetime":1751357760000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.objectstorage.UpdateBucket","source":"objectstorage","id":"a1b2c3d4-0000-4000-8000-000000000011","time":"2026-07-01T08:16:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"_Audit"},"data":{"eventName":"UpdateBucket","compartmentId":"ocid1.compartment.oc1..aaaaaaaaprod","compartmentName":"prod","resourceName":"customer-exports","resourceId":"ocid1.bucket.oc1..aaaaaaaaexports","identity":{"principalName":"contractor@acme.com","principalId":"ocid1.user.oc1..aaaaaaaacontractor","authType":"natv","tenantId":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","ipAddress":"198.51.100.77","userAgent":"OracleCLI/3.45.0"},"request":{"id":"req-0011","path":"/n/acmens/b/customer-exports","action":"PUT"},"response":{"status":"200","responseTime":"2026-07-01T08:16:00.600Z"},"additionalDetails":{"publicAccessType":"ObjectRead","namespace":"acmens","versioning":"Disabled"}}}} +{"datetime":1751357820000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.objectstorage.getobject","source":"objectstorage","id":"a1b2c3d4-0000-4000-8000-000000000012","time":"2026-07-01T08:17:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"ocid1.loggroup.oc1.iad.aaaaaaaastorage","logid":"ocid1.log.oc1.iad.aaaaaaaaosread"},"data":{"message":"GetObject failed: NotAuthenticated","bucketName":"finance-archive","objectName":"2026/q2/ledger.xlsx","namespace":"acmens","principalId":"ocid1.user.oc1..aaaaaaaaunknown","clientIpAddress":"192.0.2.66","opcRequestId":"req-0012","status":"403","action":"GET","userAgent":"python-requests/2.32.3"}}} +{"datetime":1751357880000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.identityControlPlane.DeletePolicy","source":"identityControlPlane","id":"a1b2c3d4-0000-4000-8000-000000000013","time":"2026-07-01T08:18:00.000Z","oracle":{"compartmentid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"_Audit"},"data":{"eventName":"DeletePolicy","compartmentId":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","compartmentName":"acme","resourceName":"prod-guardrails","resourceId":"ocid1.policy.oc1..aaaaaaaaguardrails","identity":{"principalName":"contractor@acme.com","principalId":"ocid1.user.oc1..aaaaaaaacontractor","authType":"natv","tenantId":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","ipAddress":"198.51.100.77","userAgent":"OracleCLI/3.45.0"},"request":{"id":"req-0013","path":"/20160918/policies/ocid1.policy.oc1..aaaaaaaaguardrails","action":"DELETE"},"response":{"status":"204","responseTime":"2026-07-01T08:18:00.500Z"}}}} +{"datetime":1751357940000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.databaseservice.autonomous.database.ChangeAutonomousDatabaseAdminPassword","source":"databaseservice","id":"a1b2c3d4-0000-4000-8000-000000000014","time":"2026-07-01T08:19:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"_Audit"},"data":{"eventName":"ChangeAutonomousDatabaseAdminPassword","compartmentId":"ocid1.compartment.oc1..aaaaaaaaprod","compartmentName":"prod","resourceName":"atp-prod-01","resourceId":"ocid1.autonomousdatabase.oc1.iad.aaaaaaaaatpprod","identity":{"principalName":"contractor@acme.com","principalId":"ocid1.user.oc1..aaaaaaaacontractor","authType":"natv","tenantId":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","ipAddress":"198.51.100.77","userAgent":"OracleCLI/3.45.0"},"request":{"id":"req-0014","path":"/20160918/autonomousDatabases/ocid1.autonomousdatabase.oc1.iad.aaaaaaaaatpprod/actions/changeAdminPassword","action":"POST"},"response":{"status":"200","responseTime":"2026-07-01T08:19:01.800Z"}}}} +{"datetime":1751358000000,"logContent":{"specversion":"1.0","type":"com.oraclecloud.integration.DeleteIntegrationInstance","source":"integration","id":"a1b2c3d4-0000-4000-8000-000000000015","time":"2026-07-01T08:20:00.000Z","oracle":{"compartmentid":"ocid1.compartment.oc1..aaaaaaaaprod","tenantid":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","loggroupid":"_Audit"},"data":{"eventName":"DeleteIntegrationInstance","compartmentId":"ocid1.compartment.oc1..aaaaaaaaprod","compartmentName":"prod","resourceName":"oic-prod","resourceId":"ocid1.integrationinstance.oc1.iad.aaaaaaaaoicprod","identity":{"principalName":"contractor@acme.com","principalId":"ocid1.user.oc1..aaaaaaaacontractor","authType":"natv","tenantId":"ocid1.tenancy.oc1..aaaaaaaatenancyexample","ipAddress":"198.51.100.77","userAgent":"OracleCLI/3.45.0"},"request":{"id":"req-0015","path":"/20190131/integrationInstances/ocid1.integrationinstance.oc1.iad.aaaaaaaaoicprod","action":"DELETE"},"response":{"status":"202","responseTime":"2026-07-01T08:20:02.100Z"}}}} diff --git a/integrations/oracle_cloud_oci/systemd/wazuh-oci-logs.service b/integrations/oracle_cloud_oci/systemd/wazuh-oci-logs.service new file mode 100644 index 0000000..7ddc30e --- /dev/null +++ b/integrations/oracle_cloud_oci/systemd/wazuh-oci-logs.service @@ -0,0 +1,35 @@ +[Unit] +Description=Forward Oracle Cloud Infrastructure logs into Wazuh +Documentation=https://github.com/wazuh/integrations/tree/main/integrations/oracle_cloud_oci +After=network-online.target wazuh-manager.service +Wants=network-online.target + +[Service] +Type=oneshot +User=root + +# Object Storage source. Swap the ExecStart below for the Streaming variant if +# Connector Hub delivers to a stream instead. +ExecStart=/var/ossec/wodles/oci/oci-logs.py \ + --source objectstorage \ + --bucket wazuh-oci-logs \ + --config-file /var/ossec/wodles/oci/config \ + --output analysisd \ + --state-file /var/ossec/var/run/oci-logs.state \ + --log-level INFO + +# Streaming source: +# ExecStart=/var/ossec/wodles/oci/oci-logs.py \ +# --source streaming \ +# --stream-id ocid1.stream.oc1.iad.EXAMPLE \ +# --config-file /var/ossec/wodles/oci/config \ +# --output analysisd \ +# --log-level INFO + +# The collector only needs to read OCI and write to the Wazuh queue socket. +PrivateTmp=yes +ProtectSystem=full +NoNewPrivileges=yes + +[Install] +WantedBy=multi-user.target diff --git a/integrations/oracle_cloud_oci/systemd/wazuh-oci-logs.timer b/integrations/oracle_cloud_oci/systemd/wazuh-oci-logs.timer new file mode 100644 index 0000000..d657ce4 --- /dev/null +++ b/integrations/oracle_cloud_oci/systemd/wazuh-oci-logs.timer @@ -0,0 +1,14 @@ +[Unit] +Description=Poll Oracle Cloud Infrastructure logs for Wazuh +Requires=wazuh-oci-logs.service + +[Timer] +# Connector Hub batches on its own schedule, so polling faster than a minute +# rarely helps. Raise this if your bucket or stream is low volume. +OnBootSec=2min +OnUnitActiveSec=1min +AccuracySec=15s +Unit=wazuh-oci-logs.service + +[Install] +WantedBy=timers.target