Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="UTF-8" ?>
<WorkflowParameterValues xmlns="http://qradar.ibm.com/UniversalCloudRESTAPI/WorkflowParameterValues/V1">
<Value name="hostname" value="api.cyble.ai" />
<Value name="api_key" value="" />
<Value name="fetch_since" value="1" />
<Value name="ioc_type" value="Domain,IPv4,URL" />
<Value name="risk_gte" value="70" />
<Value name="risk_lte" value="100" />
<Value name="regions" value="" />
<Value name="industries" value="" />
</WorkflowParameterValues>
141 changes: 141 additions & 0 deletions Community Developed/Cyble IOC/Cyble-IOC-Workflow.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
<?xml version="1.0" encoding="UTF-8"?>
<Workflow name="Cyble IOC" version="2.0"
xmlns="http://qradar.ibm.com/UniversalCloudRESTAPI/Workflow/V1">

<!-- Reading required parameters for the Workflow-->
<Parameters>
<Parameter name="hostname" label="Hostname" required="true" secret="true" />
<Parameter name="api_key" label="Api Key" required="true" secret="true" />
<Parameter name="fetch_since" label="Fetch Since (days)" required="true" />
<Parameter name="ioc_type" label="IOC Types (comma separated, no spaces, e.g. Domain,IPv4,URL)" required="true" />
<Parameter name="risk_gte" label="Risk Rating Gte" required="true" />
<Parameter name="risk_lte" label="Risk Rating Lte" required="true" />
<Parameter name="regions" label="Regions filter (comma separated, optional)" required="false" />
<Parameter name="industries" label="Industries filter (comma separated, optional)" required="false" />
</Parameters>

<Actions>

<!-- Initializing parameters-->
<Initialize path="/isFirstFetch" value="True" />
<Initialize path="/fromDate" value="" />
<Initialize path="/toDate" value="" />
<Initialize path="/fetch_since_int" value="1" />

<!-- Checking whether the current run is fetching for the first time-->
<If condition="/isFirstFetch = 'True'">
<Set path="/fetch_since_int" value="${/fetch_since}" />
<Set path="/fromDate" value="${time() - /fetch_since_int * 24 * 60 * 60 * 1000}" />
<Set path="/isFirstFetch" value="False" />
<Log type="INFO" message="CybleIOC:: Fetching IOCs for the first time." />
</If>
<Else>
<Set path="/fromDate" value="${/toDate + 1}" />
</Else>

<Set path="/toDate" value="${time() - 1}" />

<!-- The IOC API expects date-only (yyyy-MM-dd) boundaries-->
<FormatDate pattern="yyyy-MM-dd" timeZone="UTC" time="${/fromDate}" savePath="/fromDateFormatted" />
<FormatDate pattern="yyyy-MM-dd" timeZone="UTC" time="${/toDate}" savePath="/toDateFormatted" />

<Log type="INFO" message="CybleIOC:: Fetching IOCs from: ${/fromDateFormatted} to: ${/toDateFormatted}" />

<!-- The API accepts only a single iocType per request, so we split the
comma-separated list and run one paginated pass per type.-->
<Split value="${/ioc_type}" delimiter="," savePath="/ioc_type_list" />

<ForEach item="/type" items="/ioc_type_list">

<!-- Persistent per-type de-dup state: epoch SECONDS of the newest IOC of
this type already ingested. Because the API filters by date only, the
incremental window still overlaps on the current day, so this mark is
used to avoid re-posting IOCs already ingested in a previous run.-->
<Initialize path="/highWater_${/type}" value="0" />

<Log type="INFO" message="CybleIOC:: Processing type '${/type}' (highWater=${/highWater_${/type}})" />

<!-- Pagination + de-dup controls. Keep the page size modest so each request
returns quickly and avoids upstream "504: Gateway Timeout".-->
<Set path="/page" value="1" />
<Set path="/limit" value="100" />
<Set path="/hasMore" value="True" />
<Set path="/newHighWater" value="${/highWater_${/type}}" />

<While condition="/hasMore = 'True'">
<CallEndpoint url="https://${/hostname}/engine/api/v4/y/iocs" method="POST" savePath="/get_iocs">
<RequestHeader name="Accept" value="application/json" />
<RequestHeader name="Authorization" value="Bearer ${/api_key}" />
<RequestHeader name="Content-Type" value="application/json" />
<RequestBody type="application/json" encoding="UTF-8">
{
"iocType": "${/type}",
"riskRating": { "gte": "${/risk_gte}", "lte": "${/risk_lte}" },
"regions": "${/regions}",
"industries": "${/industries}",
"dateFilterBy": "last_seen",
"startDate": "${/fromDateFormatted}",
"endDate": "${/toDateFormatted}",
"sortBy": "last_seen",
"order": "desc",
"countOnly": false,
"limit": ${/limit},
"page": ${/page}
}
</RequestBody>
</CallEndpoint>

<!-- Handling error-->
<If condition="${/get_iocs/status_code} != 200">
<Log type="DEBUG" message="CybleIOC:: API Request Failed. Reason: ${/get_iocs/status_message}" />
<Abort reason="${/get_iocs/status_code}: ${/get_iocs/status_message}" />
</If>

<Set path="/batch_count" value="${count(/get_iocs/body/data/iocs)}" />
<Set path="/reachedOld" value="False" />
<Set path="/posted_count" value="0" />

<!-- Results are sorted by last_seen desc, so anything at or below the
high-water mark has already been ingested in a previous run.-->
<ForEach item="/current_ioc" items="/get_iocs/body/data/iocs">
<If condition="${/current_ioc/last_seen} > ${/highWater_${/type}}">
<PostEvent path="/current_ioc" source="${/hostname}" />
<Set path="/posted_count" value="${/posted_count + 1}" />
<If condition="${/current_ioc/last_seen} > ${/newHighWater}">
<Set path="/newHighWater" value="${/current_ioc/last_seen}" />
</If>
</If>
<Else>
<Set path="/reachedOld" value="True" />
</Else>
</ForEach>

<Log type="INFO" message="CybleIOC:: '${/type}' page ${/page}: fetched ${/batch_count}, posted ${/posted_count} new." />

<!-- Stop paging on a partial/empty page, or once we reach already-seen IOCs-->
<If condition="${/batch_count} &lt; ${/limit}">
<Set path="/hasMore" value="False" />
</If>
<Else>
<If condition="/reachedOld = 'True'">
<Set path="/hasMore" value="False" />
</If>
<Else>
<Set path="/page" value="${/page + 1}" />
</Else>
</Else>
</While>

<!-- Persist this type's newest last_seen for the next run-->
<Set path="/highWater_${/type}" value="${/newHighWater}" />
</ForEach>
</Actions>

<!-- Running tests-->
<Tests>
<DNSResolutionTest host="${/hostname}" />
<TCPConnectionTest host="${/hostname}" />
<SSLHandshakeTest host="${/hostname}"/>
</Tests>

</Workflow>
101 changes: 101 additions & 0 deletions Community Developed/Cyble IOC/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
Author Name: Cyble Inc

Maintainer Name: developers@cyble.com

Version Number: 1.0

Endpoint Documentation: This workflow can be used to pull Indicators of Compromise (IOCs) from Cyble Vision.

Detailed documentation can be found at: https://cyble.ai/centers/help-center

Event Types Currently Supported by the workflow: Cyble IOCs (Domain, IPv4, URL, Hash, etc.)

## Workflow Parameter Description

For integrating QRadar with Cyble Vision via the workflow, you will need the following information:

| **Parameter Label** | **Parameter** | **Description** |
|---------------------|---------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------|
| Hostname | hostname | Enter _hostname_ of Cyble Vision (e.g. `api.cyble.ai`). Contact your Customer Success Manager to get this value. |
| Api Key | api_key | Enter _api_key_ for authentication and fetching IOCs from Cyble. Contact your Customer Success Manager to get this value. |
| Fetch Since (days) | fetch_since | Number of days of history to retrieve on the first run. |
| IOC Types | ioc_type | A **comma-separated** list of IOC types to fetch (no spaces), e.g. `Domain` or `Domain,IPv4,URL`. Use the exact type labels returned by the API (the `ioc_type` field). The API accepts only one type per request, so the workflow runs one paginated pass per type. |
| Risk Rating Gte | risk_gte | Lower bound (inclusive) of the IOC `risk_score` filter, e.g. `70`. |
| Risk Rating Lte | risk_lte | Upper bound (inclusive) of the IOC `risk_score` filter, e.g. `100`. |
| Regions filter | regions | Comma-separated list of regions to filter on, e.g. `Asia & Pacific (APAC)`. Leave **empty** to not filter by region. |
| Industries filter | industries | Comma-separated list of industries to filter on, e.g. `BFSI,Healthcare`. Leave **empty** to not filter by industry. |

> Note: `regions` and `industries` are always sent in the request; an **empty** value is ignored by the API (equivalent to no filter), so they are safe to leave blank.

## Discovering valid filter values

Cyble exposes a filters endpoint that returns the current valid values accepted by the IOC request body. Use it to look up the exact labels to put into `ioc_type`, `regions`, `industries`, and the other filters. It requires the same Bearer token as the workflow:

```bash
curl -X 'GET' \
'https://api.cyble.ai/engine/api/v2/y/iocs/filters' \
-H 'accept: application/json' \
-H 'Authorization: Bearer <API_KEY>'
```

The response is `{"data": { ... }, "success": true}`, where each key under `data` is a list of `{ "key": ..., "name": ... }` options. Use the **`key`** value in the request body. The categories map to the IOC request body as follows:

| Filters endpoint category | IOC request body field | Notes |
|---------------------------|-------------------------|------------------------------------------|
| `types` | `iocType` | Exposed as the `ioc_type` parameter |
| `regions` | `regions` | Exposed as the `regions` parameter |
| `industries` | `industries` | Exposed as the `industries` parameter |
| `confidence_ratings` | `confidentRating` | Not yet exposed as a parameter |
| `countries` | `countryCodes` | Not yet exposed as a parameter |
| `sources` | `sources` | Not yet exposed as a parameter |
| `threat_actors` | `threatActors` | Not yet exposed as a parameter |
| `threat_malwares` | `malwareFamilies` | Not yet exposed as a parameter |
| `behaviour_tags` | `tags` | Not yet exposed as a parameter |
| `is_whitelisted` | `isWhitelisted` | Not yet exposed as a parameter |

Current values for the smaller enumerations (larger lists such as `countries`, `sources`, `threat_actors`, `threat_malwares`, and `behaviour_tags` should be fetched live from the endpoint):

- **`types`** (use for `ioc_type`): `Domain`, `Email`, `FileHash-MD5`, `FileHash-SHA1`, `FileHash-SHA256`, `IPv4`, `IPv6`, `URL`, `Wallet-Address`
- **`regions`**: `Asia & Pacific (APAC)`, `Australia and New Zealand (ANZ)`, `Europe & UK`, `Middle East & Africa (MEA)`, `North America (NA)`, `South America (SA)`, `Worldwide`
- **`industries`**: `Aerospace & Defense`, `Agriculture & Livestock`, `Automotive`, `BFSI`, `Chemicals`, `Construction`, `Consumer Goods`, `Critical Infrastructure`, `Education`, `Energy & Utilities`, `Food & Beverages`, `Government & LEA`, `Healthcare`, `Hospitality`, `IT & ITES`, `Manufacturing`, `Media & Entertainment`, `Metals Minerals & Mining`, `Multiple`, `Organisation`, `Pharmaceuticals & Biotechnology`, `Professional Services`, `Real Estate`, `Retail`, `Technology`, `Telecommunication`, `Transportation & Logistics`
- **`confidence_ratings`**: `High`, `Medium`, `Low`

## QRadar Log Source Configuration

Cyble IOC Workflow utilizes QRadar's Universal Cloud REST API Protocol to fetch data from Cyble Vision.

The steps to configure a log source on the QRadar® Console using the Workflow field are as follows:

1. Log in to QRadar.
2. Admin Panel > click on the 'QRadar Log Source Management' or 'Log Sources' app icon.
3. Click 'Log Sources' > 'New Log Source' > 'Single Log Source'.
4. On the 'Select Log Source Type' page, select __Universal DSM__ from the list.
5. On the 'Select Protocol Type' page, select __Universal Cloud REST API__, and proceed to next step.
6. On the 'Configure the Log Source parameters' page, configure the log source parameters:
- __Name__: __Cyble IOC__
- __Enabled__: __On__
- __Coalescing Events__: __Off__
- Keep rest of the settings as default, and proceed to next step.
7. On the Configure the Protocol Parameters page:
- __Log source identifier__: < same as __hostname__ above >
- Copy the workflow code from __Cyble-IOC-Workflow.xml__ and paste it into the 'Workflow' field
- Copy the workflow params from __Cyble-IOC-Workflow-Parameter-Values.xml__, populate the fields and paste into the 'Workflow Parameters Values' field
- __Untrusted Certificates__: __Allow__
- Set Recurrence as per your choice. Recommended value is 1D.
- Keep rest of the settings as default, and proceed to next step.
8. In the Test protocol parameters window, click 'Start Test'. All tests should pass.
9. To fix any errors, click 'Configure Protocol Parameters'. Configure the parameters and click Test Protocol Parameters.
10. Click 'Finish'
11. Navigate to the 'Admin' tab. From the top bar choose 'Deploy Changes'

## Installing the Cyble IOC DSM Parser (optional)

By default the log source uses the **Universal DSM**, so incoming IOC events are stored with their raw JSON payload but are not normalized into QRadar fields. To have the IOC events parsed and mapped into proper QRadar properties, install the Cyble IOC DSM parser:

1. **Request the DSM export** — the parser is distributed as a DSM export archive (e.g. `Cyble-IOC-DSM-Export.zip`). Contact your **Customer Success Manager (Cyble)** to obtain the latest zip.
2. In your QRadar instance, navigate to **Admin Panel > Extensions Management > Add**, select the downloaded zip, and install it.
- Alternatively, import it from the **DSM Editor** (Admin > DSM Editor > import), if provided in that format.
3. Edit the **Cyble IOC** log source and set its **Log Source Type** to the **Cyble IOC DSM** provided by the parser (instead of Universal DSM).
4. Navigate to the 'Admin' tab and choose **Deploy Changes**.

> Note: QRadar parses events at ingest time. Installing or updating the DSM only affects **new** events received after it is deployed; events already ingested under the Universal DSM are not retroactively re-parsed.