Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feedly/fetch reports as incidents #37229

Open
wants to merge 27 commits into
base: contrib/Mathieu4141_feedly/fetch-reports-as-incidents
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
3cb67f0
[feedly] Fix new intrusion set objects
Mathieu4141 May 21, 2024
8550c2a
[feedly] update image version
Mathieu4141 May 21, 2024
7c21a9c
[feedly] parse vulnerabilities
Mathieu4141 May 22, 2024
349022c
[feedly] update version
Mathieu4141 May 22, 2024
01a4086
[feedly] (incidents) WIP
Mathieu4141 Oct 7, 2024
2b18527
[feedly] first version of the integration to ingest reports as incidents
Mathieu4141 Oct 30, 2024
8d8eab7
[feedly] wip
Mathieu4141 Nov 14, 2024
ce8883f
[feedly] wip
Mathieu4141 Nov 14, 2024
02b022e
[feedly] wip
Mathieu4141 Nov 14, 2024
7cda2f3
[feedly] wip
Mathieu4141 Nov 14, 2024
3884bb5
[feedly] wip
Mathieu4141 Nov 14, 2024
80ff003
WIP
Mathieu4141 Nov 14, 2024
278819d
Merge branch 'master' into feedly/fetch-reports-as-incidents
Mathieu4141 Nov 14, 2024
c570e93
WIP
Mathieu4141 Nov 15, 2024
720b876
[feedly] fix newerThan for tags
Mathieu4141 Nov 15, 2024
b64838f
[feedly] fix test
Mathieu4141 Nov 19, 2024
f9e78a8
Merge branch 'contrib/Mathieu4141_feedly/fetch-reports-as-incidents' …
Mathieu4141 Nov 19, 2024
2c16705
[feedly] fix coverage
Mathieu4141 Nov 19, 2024
cd61366
[feedly] fix tests
Mathieu4141 Nov 19, 2024
2ca569a
[feedly] Add separate pack for articles as incidents
Mathieu4141 Dec 24, 2024
c5fcaa4
[feedly] Fix version & add release notes
Mathieu4141 Jan 10, 2025
0cb2698
[feedly] PR review
Mathieu4141 Jan 16, 2025
f6a289e
[feedly] format files
Mathieu4141 Jan 16, 2025
ecea6d5
[feedly] pr.review: 4. unsearchable
Mathieu4141 Jan 20, 2025
1ac30a5
[feedly] pr.review: 3. 5. Associate fields with Feedly Report
Mathieu4141 Jan 20, 2025
6695291
[feedly] pr.review: 1. & 2. Add conditional tasks to check if indicat…
Mathieu4141 Jan 30, 2025
4bd5197
[feedly] pr.review: Add playbook png
Mathieu4141 Feb 10, 2025
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
63 changes: 49 additions & 14 deletions Packs/FeedFeedly/Integrations/FeedFeedly/FeedFeedly.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import copy
from contextlib import suppress
from typing import Any
from urllib.parse import parse_qs

from CommonServerPython import * # noqa: F401
Expand Down Expand Up @@ -115,7 +116,9 @@


class Client(BaseClient):
def fetch_indicators_from_stream(self, stream_id: str, newer_than: float, *, limit: Optional[int] = None) -> list:
def fetch_indicators_from_stream(
self, stream_id: str, newer_than: float, *, limit: int | None = None, ingest_reports: bool = True
) -> tuple[list, float | None]:
params = {
"streamId": stream_id,
"count": 20,
Expand All @@ -137,7 +140,7 @@ def fetch_indicators_from_stream(self, stream_id: str, newer_than: float, *, lim

demisto.debug(f"Fetched {len(objects)} objects from stream {stream_id}")

indicators = STIX2Parser().parse_stix2_objects(objects)
indicators = STIX2Parser(ingest_reports=ingest_reports).parse_stix2_objects(objects)

if limit:
indicators = indicators[:limit]
Expand All @@ -146,7 +149,7 @@ def fetch_indicators_from_stream(self, stream_id: str, newer_than: float, *, lim
indicator["type"] = indicator.get("indicator_type", "")
indicator["fields"] = indicator.get("customFields", {})

return indicators
return indicators, extract_next_newer_than(objects)


class STIX2Parser:
Expand Down Expand Up @@ -180,7 +183,7 @@ class STIX2Parser:
"vulnerability",
]

def __init__(self):
def __init__(self, ingest_reports: bool):
self.indicator_regexes = [
re.compile(INDICATOR_EQUALS_VAL_PATTERN),
re.compile(INDICATOR_IN_VAL_PATTERN),
Expand All @@ -194,6 +197,8 @@ def __init__(self):
self.id_to_object: dict[str, Any] = {}
self.parsed_object_id_to_object: dict[str, Any] = {}

self.ingest_reports = ingest_reports

@staticmethod
def get_indicator_publication(indicator: dict[str, Any]):
"""
Expand Down Expand Up @@ -300,13 +305,14 @@ def parse_attack_pattern(attack_pattern_obj: dict[str, Any]) -> list[dict[str, A

return [attack_pattern]

@staticmethod
def parse_report(report_obj: dict[str, Any]):
def parse_report(self, report_obj: dict[str, Any]) -> tuple[list[dict[str, Any]], list[dict[str, Any]]]:
"""
Parses a single report object
:param report_obj: report object
:return: report extracted from the report object in cortex format
"""
if not self.ingest_reports:
return [], []
object_refs = report_obj.get("object_refs", [])
new_relationships = []
for obj_id in object_refs:
Expand Down Expand Up @@ -957,7 +963,7 @@ def test_module(client: Client, params: dict) -> str: # pragma: no cover


def get_indicators_command(
client: Client, params: dict[str, str], args: dict[str, str]
client: Client, params: dict[str, Any], args: dict[str, Any]
) -> CommandResults: # pragma: no cover
"""Wrapper for retrieving indicators from the feed to the war-room.
Args:
Expand All @@ -967,14 +973,19 @@ def get_indicators_command(
Returns:
Outputs.
"""
indicators = client.fetch_indicators_from_stream(
params["feedly_stream_id"], newer_than=time.time() - 24 * 3600, limit=int(args.get("limit", "10"))
indicators, _ = client.fetch_indicators_from_stream(
params["feedly_stream_id"],
newer_than=time.time() - 24 * 3600,
limit=int(args.get("limit", "10")),
ingest_reports=args.get("ingest_articles_as_indicators", True),
)
demisto.createIndicators(indicators) # type: ignore
return CommandResults(readable_output=f"Created {len(indicators)} indicators.")


def fetch_indicators_command(client: Client, params: dict[str, str], context: dict[str, str]) -> list[dict]:
def fetch_indicators_command(
client: Client, params: dict[str, str], context: dict[str, str]
) -> tuple[list, float | None]: # pragma: no cover
"""Wrapper for fetching indicators from the feed to the Indicators tab.
Args:
client: Client object with request
Expand All @@ -984,10 +995,34 @@ def fetch_indicators_command(client: Client, params: dict[str, str], context: di
Indicators.
"""
return client.fetch_indicators_from_stream(
params["feedly_stream_id"], newer_than=float(context.get("last_successful_run", time.time() - 7 * 24 * 3600))
params["feedly_stream_id"],
newer_than=get_newer_than_timestamp(params, context),
ingest_reports=params.get("ingest_articles_as_indicators", True), # type: ignore
)


def get_newer_than_timestamp(params: dict[str, str], context: dict[str, str]) -> float: # pragma: no cover
stream_id = params["feedly_stream_id"]
if "/tag/" in stream_id:
saved_timestamp = context.get("last_run")
else:
saved_timestamp = context.get("last_fetched_article_crawled_time")

return float(saved_timestamp or (time.time() - int(params.get("days_to_backfill", 7)) * 24 * 3600))


def extract_next_newer_than(stix_objects: list[dict[str, str]]) -> float | None:
if not stix_objects:
return None
return datetime.fromisoformat(
max(stix_object["created"] for stix_object in stix_objects if stix_object.get("type") == "report")
).timestamp()


def set_next_newer_than(last_article_time: float, now: float) -> None: # pragma: no cover
demisto.setLastRun({"last_fetched_article_crawled_time": last_article_time, "last_run": now})


def main(): # pragma: no cover
params = demisto.params()

Expand All @@ -1012,11 +1047,11 @@ def main(): # pragma: no cover

elif command == "fetch-indicators":
now = time.time()
indicators = fetch_indicators_command(client, params, demisto.getLastRun())
indicators, last_fetched_article_time = fetch_indicators_command(client, params, demisto.getLastRun())
for indicators_batch in batch(indicators, batch_size=2000):
demisto.createIndicators(indicators_batch) # type: ignore
demisto.setLastRun({"last_successful_run": str(now)})

if indicators:
set_next_newer_than(last_fetched_article_time, now) # type: ignore
else:
raise NotImplementedError(f"Command {command} is not implemented.")

Expand Down
8 changes: 7 additions & 1 deletion Packs/FeedFeedly/Integrations/FeedFeedly/FeedFeedly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,12 @@ configuration:
type: 0
defaultvalue: '7'
additionalinfo: Number of days to fetch articles from when running the integration for the first time
- display: Ingest Articles as Indicators
name: ingest_articles_as_indicators
required: false
type: 8
defaultvalue: 'true'
additionalinfo: When selected, the integration will ingest articles as indicators. If not selected, you may want to use the IncidentsFeedly integration to ingest articles as incidents. The current FeedFeedly integration will still be needed to ingest entities as indicators, and to create relationships between indicators.
- additionalinfo: Incremental feeds pull only new or modified indicators that have been sent from the integration. The determination if the indicator is new or modified happens on the 3rd-party vendor's side, so only indicators that are new or modified are sent to Cortex XSOAR. Therefore, all indicators coming from these feeds are labeled new or modified.
defaultvalue: 'true'
display: Incremental feed
Expand Down Expand Up @@ -116,7 +122,7 @@ script:
description: Gets indicators from the feed.
execution: false
name: feedly-get-indicators
dockerimage: demisto/python3:3.10.14.94490
dockerimage: demisto/python3:3.11.9.105369
feed: true
isfetch: false
longRunning: false
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## Feedly

Use the Feedly integration to import articles with entities, indicators, and relationships from your Feedly boards and folders.
Use the FeedFeedly integration to import articles with entities, indicators, and relationships as indicators from your Feedly boards and folders.

**Note** There is a second integration `IncidentsFeedly` that can be used to ingest articles as incidents instead of indicators. This `FeedFeedly` integration is still needed, to ingest entities (intrusion sets, malware, TTPs) as indicators, and relationships between them.

**Disclaimer** You will need the Feedly for Threat Intelligence package to enable this integration. You can learn more about our product here: https://feedly.com/i/landing/threatIntelligence

Expand Down
8 changes: 6 additions & 2 deletions Packs/FeedFeedly/Integrations/FeedFeedly/FeedFeedly_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,12 @@ def test_build_iterator(requests_mock):
response = file.read()
requests_mock.get(URL, text=response)
expected_ips = {"31.31.194.65", "95.213.205.83", "77.223.124.212"}
client = Client(base_url=URL, verify=False, proxy=False,)
indicators = client.fetch_indicators_from_stream("tag/enterpriseName/category/uuid", 0)
client = Client(
base_url=URL,
verify=False,
proxy=False,
)
indicators = client.fetch_indicators_from_stream("tag/enterpriseName/category/uuid", 0)[0]
ip_indicators = {indicator["value"] for indicator in indicators if indicator["type"] == "IP"}
assert expected_ips == ip_indicators

Expand Down
1 change: 1 addition & 0 deletions Packs/FeedFeedly/Integrations/FeedFeedly/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Ingest articles with indicators, entities and relationships from Feedly into XSO
| | | False |
| Stream ID | The stream id you want to fetch articles from. You can find it in Feedly by going to the stream, clicking on \`...\` > \`Sharing\`, then \`Copy ID\` in the \`Feedly API Stream ID\` section. | True |
| Days to fetch for first run | Number of days to fetch articles from when running the integration for the first time | True |
| Ingest Articles as Indicators | When selected, the integration will ingest articles as indicators. If not selected, you may want to use the IncidentsFeedly integration to ingest articles as incidents. The current FeedFeedly integration will still be needed to ingest entities as indicators, and to create relationships between indicators. | False |
| Incremental feed | Incremental feeds pull only new or modified indicators that have been sent from the integration. The determination if the indicator is new or modified happens on the 3rd-party vendor's side, so only indicators that are new or modified are sent to Cortex XSOAR. Therefore, all indicators coming from these feeds are labeled new or modified. | False |

4. Click **Test** to validate the URLs, token, and connection.
Expand Down

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Packs/FeedFeedly/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
The Feedly feed provides access to articles with entities, IoCs, and relationships from your Feedly account.

You can ingest the articles as indicators using this pack. If you prefer to ingest them as incidents, you need to also install the FeedlyArticles pack.

In order to access the Feedly feed, you'll need to generate an access token. You can do it from [here](https://feedly.com/i/team/api).
7 changes: 7 additions & 0 deletions Packs/FeedFeedly/ReleaseNotes/1_1_0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

#### Integrations

##### Feedly Feed

- Added reference to the new incident feed integration.
- Fixed an issue with newerThan param
2 changes: 1 addition & 1 deletion Packs/FeedFeedly/pack_metadata.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "Feedly",
"description": "Import Articles from Feedly with enriched IOCs",
"support": "partner",
"currentVersion": "1.0.4",
"currentVersion": "1.1.0",
YairGlick marked this conversation as resolved.
Show resolved Hide resolved
"author": "Feedly",
"url": "https://feedly.com/i/landing/threatIntelligence",
"email": "[email protected]",
Expand Down
Empty file.
2 changes: 2 additions & 0 deletions Packs/FeedlyArticles/.secrets-ignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
https://api.feedly.com
https://feedly.com
Binary file added Packs/FeedlyArticles/Author_image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
{
"brands": null,
"cacheVersn": 0,
"defaultIncidentType": "",
"definitionId": "",
"description": "",
"feed": false,
"fromServerVersion": "",
"id": "Feedly - Report Mapper",
"incidentSamples": null,
"indicatorSamples": null,
"instanceIds": null,
"itemVersion": "",
"keyTypeMap": {},
"locked": false,
"logicalVersion": 10,
"mapping": {
"dbot_classification_incident_type_all": {
"dontMapEventToLabels": true,
"internalMapping": {
"Additional Email Addresses": {
"simple": "indicators.Email"
},
"CVE List": {
"simple": "indicators.CVE"
},
"Detected IPs": {
"simple": "indicators.IP"
},
"Domain Name": {
"simple": "indicators.Domain"
},
"Event ID": {
"simple": "event_id"
},
"Feedly Malware Names": {
"simple": "indicators.Malware"
},
"Feedly Threat Actor Names": {
"simple": "indicators.Threat Actor"
},
"Feedly crawled date": {
"simple": "create_time"
},
"Feedly url": {
"simple": "feedly_url"
},
"File MD5": {
"simple": "indicators.File"
},
"MITRE Technique ID": {
"simple": "indicators.TTP"
},
"MITRE Technique Name": {
"simple": "indicators.Attack Pattern"
},
"Source name": {
"simple": "source.name"
},
"Source url": {
"simple": "source.url"
},
"Tags": {
"complex": {
"filters": [],
"root": "tags",
"transformers": []
}
},
"Threat Name": {
"simple": "indicators.Malware"
},
"URLs": {
"simple": "indicators.URL"
},
"Use Case Description": {
"simple": "content"
},
"name": {
"simple": "name"
}
}
}
},
"name": "Feedly - Report Mapper",
"nameRaw": "Feedly - Report Mapper",
"packID": "",
"packName": "",
"propagationLabels": [
"all"
],
"sourceClassifierId": "",
"system": false,
"toServerVersion": "",
"transformer": {},
"type": "mapping-incoming",
"unclassifiedCases": null,
"version": -1
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"id": "incident_feedlycrawleddate",
"version": -1,
"modified": "2024-10-10T12:19:12.752862283Z",
"name": "Feedly crawled date",
"ownerOnly": false,
"cliName": "feedlycrawleddate",
"type": "shortText",
"closeForm": false,
"editForm": true,
"required": false,
"neverSetAsRequired": false,
"isReadOnly": false,
"useAsKpi": false,
"locked": false,
"system": false,
"content": true,
"group": 0,
"hidden": false,
"openEnded": false,
"associatedToAll": true,
"unmapped": false,
"unsearchable": false,
"caseInsensitive": true,
"sla": 0,
"threshold": 72,
"fromVersion": "6.10.0"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"id": "incident_feedlymalwarenames",
"version": -1,
"modified": "2024-11-13T14:27:30.49621132Z",
"name": "Feedly Malware Names",
"ownerOnly": false,
"cliName": "feedlymalwarenames",
"type": "multiSelect",
"closeForm": false,
"editForm": true,
"required": false,
"neverSetAsRequired": false,
"isReadOnly": false,
"useAsKpi": false,
"locked": false,
"system": false,
"content": true,
"group": 0,
"hidden": false,
"openEnded": true,
"associatedToAll": true,
"unmapped": false,
"unsearchable": false,
"caseInsensitive": true,
"sla": 0,
"threshold": 72,
"fromVersion": "6.10.0"
}
Loading
Loading