Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 30 additions & 6 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -1,26 +1,33 @@
# See https://pre-commit.com for more information
# See https://pre-commit.com/hooks.html for more hooks

# By default, run each hook only in the standard pre-commit stage
default_install_hook_types: [pre-commit, commit-msg]
default_stages: [pre-commit]

# This is a template for connector pre-commit hooks
repos:
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v4.0.0
rev: v4.2.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
args: [--verbose]
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
rev: v5.0.0
hooks:
- id: check-merge-conflict
- id: end-of-file-fixer
exclude: ^NOTICE$
exclude_types: [ markdown ]
- id: trailing-whitespace
exclude: ^NOTICE$
exclude_types: [ markdown ]
- id: requirements-txt-fixer
- id: check-json
- id: check-yaml
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.9.6
rev: v0.12.0
hooks:
- id: ruff
args: [ "--fix", "--unsafe-fixes"] # Allow unsafe fixes (ruff pretty strict about what it can fix)
Expand All @@ -36,7 +43,7 @@ repos:
- id: mdformat
exclude: "release_notes/.*"
- repo: https://github.com/returntocorp/semgrep
rev: v1.107.0
rev: v1.89.0
hooks:
- id: semgrep
- repo: https://github.com/Yelp/detect-secrets
Expand All @@ -51,8 +58,25 @@ repos:
rev: v2.0.4
hooks:
- id: build-docs
language: python
additional_dependencies: ["local-hooks"]
args: ['.']
- id: copyright
# - id: package-app-dependencies
# - id: notice-file
language: python
additional_dependencies: ["local-hooks"]
args: ['.']
- id: package-app-dependencies
language: python
additional_dependencies: ["local-hooks"]
- id: notice-file
language: python
additional_dependencies: ["local-hooks"]
args: ['.']
- id: release-notes
language: python
additional_dependencies: ["local-hooks"]
args: ['.']
- id: static-tests
language: python
additional_dependencies: ["local-hooks"]
args: ['.']
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.

Copyright 2025 Splunk Inc.
Copyright 2025 Google LLC

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
2 changes: 2 additions & 0 deletions NOTICE
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Splunk SOAR App: Google Threat Intelligence
Copyright 2025 Google LLC
3,333 changes: 3,326 additions & 7 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion __init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# File: __init__.py
#
# Copyright (c) 2025 Splunk Inc.
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand Down
23 changes: 15 additions & 8 deletions googlethreatintelligence_consts.py → actions/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# File: googlethreatintelligence_consts.py
# File: __init__.py
#
# Copyright (c) 2025 Splunk Inc.
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
Expand All @@ -13,11 +13,18 @@
# either express or implied. See the License for the specific language governing permissions
# and limitations under the License.

# API endpoints
GOOGLETHREATINTELLIGENCE_CONNECTIVITY_ENDPOINT = "/endpoint/to/test/connectivity"
from phantom.action_result import ActionResult

# Error messages
GOOGLETHREATINTELLIGENCE_ERR_CONNECTIVITY_TEST = "Test Connectivity Failed"

# Success messages
GOOGLETHREATINTELLIGENCE_SUCC_CONNECTIVITY_TEST = "Test Connectivity Passed"
class BaseAction:
"""Base Action class to generate the action objects."""

def __init__(self, connector, param):
"""Prepare constructor for actions.

:param connector: Vision connector object
:param param: Parameter dictionary
"""
self._connector = connector
self._action_result = connector.add_action_result(ActionResult(dict(param)))
self._param = param
154 changes: 154 additions & 0 deletions actions/google_threat_intelligence_add_comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
# File: google_threat_intelligence_add_comment.py
#
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
# either express or implied. See the License for the specific language governing permissions
# and limitations under the License.

from urllib.parse import urlencode

import phantom.app as phantom

import google_threat_intelligence_consts as consts
from actions import BaseAction


class AddComment(BaseAction):
"""Class to handle add comment action."""

def execute(self):
"""Execute add comment action.

Step 1: Validate parameters
Step 2: Get query params, Optional
Step 3: Get headers, Optional
Step 4: Get request body, Optional
Step 5: Get request url
Step 6: Invoke API
Step 7: Handle the response
"""
self._connector.save_progress(consts.EXECUTION_START_MSG.format("add_comment"))

entity = self._param["entity"]
password = self._param.get("password")
comment_text = self._param.get("comment_text")

entity_type = self._connector.util.get_data_type(entity)
request_body = self.__get_request_body(comment_text)

self._connector.debug_print(f"Running action add_comment with {entity} and entity type {entity_type}.")

if entity_type == consts.IP_ADDRESS or entity_type == consts.DOMAIN:
endpoint, method = (
consts.ADD_COMMENT_ENDPOINT.format(entity_type=entity_type, entity=entity),
"post",
)

elif entity_type == consts.URL:
ret_val, url_id = self._connector.util.submit_url_for_analysis(self._action_result, entity)
if phantom.is_fail(ret_val):
return self._action_result.get_status()

endpoint, method = (
consts.ADD_COMMENT_ENDPOINT.format(entity_type=entity_type, entity=url_id),
"post",
)

elif entity_type == consts.FILE:
ret_val, file_id = self._connector.util.get_file_hash_from_input(self._action_result, entity, password)
if phantom.is_fail(ret_val):
return self._action_result.get_status()

endpoint, method = (
consts.ADD_COMMENT_ENDPOINT.format(entity_type=entity_type, entity=file_id),
"post",
)

else:
return self._action_result.set_status(phantom.APP_ERROR, consts.ERROR_MESSAGE_INVALID_ENTITY)

self._connector.debug_print(f"endpoint: {endpoint}, method: {method}")
ret_val, response = self._make_rest_call(url=endpoint, method=method, body=request_body)

if phantom.is_fail(ret_val):
return self._action_result.get_status()

return self.__handle_response(ret_val, response)

def __get_request_body(self, comment_text):
"""
Generates a request body for adding a comment.

Args:
comment_text (str): The text of the comment to be added.

Returns:
dict: The generated request body.
"""
body = {"data": {"type": "comment", "attributes": {"text": f"{comment_text}"}}}
allow_none = []
allow_empty = {}
default_values = {}

return self._connector.util.generate_json_body(body, allow_none, allow_empty, self._param, default_values)

def _make_rest_call(self, url, method, headers=None, param=None, body=None):
"""
Invoke API.

Args:
url (str): The URL to make the REST call to.
method (str): The method to use for the REST call.
headers (dict): The headers to be sent in the request.
param (dict): The query parameters to be sent in the request.
body (dict): The request body to be sent in the request.

Returns:
tuple: A tuple containing the status of the processing and the data to return.
"""
args = {
"endpoint": url,
"action_result": self._action_result,
"method": method.lower(),
"headers": headers or {},
}

if param:
args["endpoint"] = f"{args['endpoint']}?{urlencode(param)}"

if body:
args["json"] = body

return self._connector.util.make_rest_call(**args)

def __handle_response(self, ret_val, response):
"""Process response received from the third party API

Args:
ret_val (RetVal): The return value from the REST call.
response (dict or list): The response data received from the third party API.

Returns:
tuple: A tuple containing the status of the processing and the data to return.
"""
if phantom.is_fail(ret_val):
return self._action_result.get_status()

if isinstance(response, list):
for item in response:
self._action_result.add_data(item)
else:
self._action_result.add_data(response)

return self._action_result.set_status(
phantom.APP_SUCCESS,
consts.ACTION_ADD_COMMENT_SUCCESS_RESPONSE,
)
99 changes: 99 additions & 0 deletions actions/google_threat_intelligence_delete_comment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# File: google_threat_intelligence_delete_comment.py
#
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software distributed under
# the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
# either express or implied. See the License for the specific language governing permissions
# and limitations under the License.

from urllib.parse import urlencode

import phantom.app as phantom

import google_threat_intelligence_consts as consts
from actions import BaseAction


class DeleteComment(BaseAction):
"""Class to handle delete comment action."""

def execute(self):
"""Execute delete comment action.

Step 1: Validate parameters
Step 2: Get query params, Optional
Step 3: Get headers, Optional
Step 4: Get request body, Optional
Step 5: Get request url
Step 6: Invoke API
Step 7: Handle the response
"""
self._connector.save_progress(consts.EXECUTION_START_MSG.format("delete_comment"))

comment_id = self._param.get("comment_id")
endpoint, method = consts.DELETE_COMMENT_ENDPOINT.format(comment_id=comment_id), "delete"

ret_val, response = self._make_rest_call(url=endpoint, method=method)

return self.__handle_response(ret_val, response)

def _make_rest_call(self, url, method, headers=None, param=None, body=None):
"""
Make a REST call to the API.

Args:
url (str): The endpoint to make the REST call to.
method (str): The method to use for the REST call.
headers (dict, optional): The headers to be sent in the request. Defaults to {}.
param (dict, optional): The query parameters to be sent in the request. Defaults to {}.
body (dict, optional): The request body to be sent in the request. Defaults to {}.

Returns:
tuple: A tuple containing the status of the processing and the data to return.
"""
args = {
"endpoint": url,
"action_result": self._action_result,
"method": method.lower(),
"headers": headers or {},
}

if param:
args["endpoint"] = f"{args['endpoint']}?{urlencode(param)}"

if body:
args["json"] = body

return self._connector.util.make_rest_call(**args)

def __handle_response(self, ret_val, response):
"""
Process response received from the third party API

Args:
ret_val (RetVal): The return value from the REST call.
response (dict or list): The response data received from the third party API.

Returns:
tuple: A tuple containing the status of the processing and the data to return.
"""
if phantom.is_fail(ret_val):
return self._action_result.get_status()

if isinstance(response, list):
for item in response:
self._action_result.add_data(item)
else:
self._action_result.add_data(response)

return self._action_result.set_status(
phantom.APP_SUCCESS,
consts.ACTION_DELETE_COMMENT_SUCCESS_RESPONSE,
)
Loading