mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-01-25 02:08:11 +00:00
379 lines
16 KiB
Plaintext
379 lines
16 KiB
Plaintext
---
|
||
title: 'Creating a New Integration'
|
||
---
|
||
|
||
## Introduction
|
||
|
||
Integrating Prowler with external tools enhances its functionality and enables seamless workflow automation. Prowler supports a variety of integrations to optimize security assessments and reporting.
|
||
|
||
### Supported Integration Targets
|
||
|
||
- Messaging Platforms – Example: Slack
|
||
|
||
- Project Management Tools – Example: Jira
|
||
|
||
- Cloud Services – Example: AWS Security Hub
|
||
|
||
### Integration Guidelines
|
||
To integrate Prowler with a specific product:
|
||
|
||
Refer to the [Prowler Developer Guide](https://docs.prowler.com/projects/prowler-open-source/en/latest/) to understand its architecture and integration mechanisms.
|
||
|
||
* Identify the most suitable integration method for the intended platform.
|
||
|
||
## Steps to Create an Integration
|
||
|
||
### Defining the Integration Purpose
|
||
|
||
* Before implementing an integration, clearly define its objective. Common purposes include:
|
||
|
||
* Sending Prowler findings to a platform for alerting, tracking, or further analysis.
|
||
* For inspiration and implementation examples, please review the existing integrations in the [`prowler/lib/outputs`](https://github.com/prowler-cloud/prowler/tree/master/prowler/lib/outputs) folder.
|
||
|
||
### Developing the Integration
|
||
|
||
* Script Development:
|
||
|
||
* Write a script to process Prowler’s output and interact with the target platform’s API.
|
||
* If the goal is to send findings, parse Prowler’s results and use the platform’s API to create entries or notifications.
|
||
|
||
* Configuration:
|
||
|
||
* Ensure the script supports environment-specific settings, such as:
|
||
|
||
- API endpoints
|
||
|
||
- Authentication tokens
|
||
|
||
- Any necessary configurable parameters.
|
||
|
||
### Fundamental Structure
|
||
|
||
* Integration Class:
|
||
|
||
* To implement an integration, create a class that encapsulates the required attributes and methods for interacting with the target platform. Example: Jira Integration
|
||
|
||
```python title="Jira Class"
|
||
class Jira:
|
||
"""
|
||
Jira class to interact with the Jira API
|
||
|
||
[Note]
|
||
This integration is limited to a single Jira Cloud instance, meaning all issues will be created under the same Jira Cloud ID. Future improvements will include the ability to specify a Jira Cloud ID for users associated with multiple accounts.
|
||
|
||
Attributes
|
||
- _redirect_uri: The redirect URI used
|
||
- _client_id: The client identifier
|
||
- _client_secret: The client secret
|
||
- _access_token: The access token
|
||
- _refresh_token: The refresh token
|
||
- _expiration_date: The authentication expiration
|
||
- _cloud_id: The cloud identifier
|
||
- _scopes: The scopes needed to authenticate, read:jira-user read:jira-work write:jira-work
|
||
- AUTH_URL: The URL to authenticate with Jira
|
||
- PARAMS_TEMPLATE: The template for the parameters to authenticate with Jira
|
||
- TOKEN_URL: The URL to get the access token from Jira
|
||
- API_TOKEN_URL: The URL to get the accessible resources from Jira
|
||
|
||
Methods
|
||
__init__: Initializes the Jira object
|
||
- input_authorization_code: Inputs the authorization code
|
||
- auth_code_url: Generates the URL to authorize the application
|
||
- get_auth: Gets the access token and refreshes it
|
||
- get_cloud_id: Gets the cloud identifier from Jira
|
||
- get_access_token: Gets the access token
|
||
- refresh_access_token: Refreshes the access token from Jira
|
||
- test_connection: Tests the connection to Jira and returns a Connection object
|
||
- get_projects: Gets the projects from Jira
|
||
- get_available_issue_types: Gets the available issue types for a project
|
||
- send_findings: Sends the findings to Jira and creates an issue
|
||
|
||
Raises:
|
||
- JiraGetAuthResponseError: Failed to get the access token and refresh token
|
||
- JiraGetCloudIDNoResourcesError: No resources were found in Jira when getting the cloud id
|
||
- JiraGetCloudIDResponseError: Failed to get the cloud ID, response code did not match 200
|
||
- JiraGetCloudIDError: Failed to get the cloud ID from Jira
|
||
- JiraAuthenticationError: Failed to authenticate
|
||
- JiraRefreshTokenError: Failed to refresh the access token
|
||
- JiraRefreshTokenResponseError: Failed to refresh the access token, response code did not match 200
|
||
- JiraGetAccessTokenError: Failed to get the access token
|
||
- JiraNoProjectsError: No projects found in Jira
|
||
- JiraGetProjectsError: Failed to get projects from Jira
|
||
- JiraGetProjectsResponseError: Failed to get projects from Jira, response code did not match 200
|
||
- JiraInvalidIssueTypeError: The issue type is invalid
|
||
- JiraGetAvailableIssueTypesError: Failed to get available issue types from Jira
|
||
- JiraGetAvailableIssueTypesResponseError: Failed to get available issue types from Jira, response code did not match 200
|
||
- JiraCreateIssueError: Failed to create an issue in Jira
|
||
- JiraSendFindingsResponseError: Failed to send the findings to Jira
|
||
- JiraTestConnectionError: Failed to test the connection
|
||
|
||
Usage:
|
||
jira = Jira(
|
||
redirect_uri="http://localhost:8080",
|
||
client_id="client_id",
|
||
client_secret="client_secret
|
||
)
|
||
jira.send_findings(findings=findings, project_key="KEY")
|
||
"""
|
||
|
||
_redirect_uri: str = None
|
||
_client_id: str = None
|
||
_client_secret: str = None
|
||
_access_token: str = None
|
||
_refresh_token: str = None
|
||
_expiration_date: int = None
|
||
_cloud_id: str = None
|
||
_scopes: list[str] = None
|
||
AUTH_URL = "https://auth.atlassian.com/authorize"
|
||
PARAMS_TEMPLATE = {
|
||
"audience": "api.atlassian.com",
|
||
"client_id": None,
|
||
"scope": None,
|
||
"redirect_uri": None,
|
||
"state": None,
|
||
"response_type": "code",
|
||
"prompt": "consent",
|
||
}
|
||
TOKEN_URL = "https://auth.atlassian.com/oauth/token"
|
||
API_TOKEN_URL = "https://api.atlassian.com/oauth/token/accessible-resources"
|
||
|
||
def __init__(
|
||
self,
|
||
redirect_uri: str = None,
|
||
client_id: str = None,
|
||
client_secret: str = None,
|
||
):
|
||
self._redirect_uri = redirect_uri
|
||
self._client_id = client_id
|
||
self._client_secret = client_secret
|
||
self._scopes = ["read:jira-user", "read:jira-work", "write:jira-work"]
|
||
auth_url = self.auth_code_url()
|
||
authorization_code = self.input_authorization_code(auth_url)
|
||
self.get_auth(authorization_code)
|
||
|
||
# More properties and methods
|
||
```
|
||
|
||
* Test Connection Method:
|
||
|
||
* Validating Credentials or Tokens
|
||
|
||
To ensure a successful connection to the target platform, implement a method that validates authentication credentials or tokens.
|
||
|
||
#### Method Implementation
|
||
|
||
The following example demonstrates the `test_connection` method for the `Jira` class:
|
||
|
||
```python title="Test connection"
|
||
@staticmethod
|
||
def test_connection(
|
||
redirect_uri: str = None,
|
||
client_id: str = None,
|
||
client_secret: str = None,
|
||
raise_on_exception: bool = True,
|
||
) -> Connection:
|
||
"""Test the connection to Jira
|
||
|
||
Args:
|
||
- redirect_uri: The redirect URI used
|
||
- client_id: The client identifier
|
||
- client_secret: The client secret
|
||
- raise_on_exception: Whether to raise an exception or not
|
||
|
||
Returns:
|
||
- Connection: The connection object
|
||
|
||
Raises:
|
||
- JiraGetCloudIDNoResourcesError: No resources were found in Jira when getting the cloud id
|
||
- JiraGetCloudIDResponseError: Failed to get the cloud ID, response code did not match 200
|
||
- JiraGetCloudIDError: Failed to get the cloud ID from Jira
|
||
- JiraAuthenticationError: Failed to authenticate
|
||
- JiraTestConnectionError: Failed to test the connection
|
||
"""
|
||
try:
|
||
jira = Jira(
|
||
redirect_uri=redirect_uri,
|
||
client_id=client_id,
|
||
client_secret=client_secret,
|
||
)
|
||
access_token = jira.get_access_token()
|
||
|
||
if not access_token:
|
||
return ValueError("Failed to get access token")
|
||
|
||
headers = {"Authorization": f"Bearer {access_token}"}
|
||
response = requests.get(
|
||
f"https://api.atlassian.com/ex/jira/{jira.cloud_id}/rest/api/3/myself",
|
||
headers=headers,
|
||
)
|
||
|
||
if response.status_code == 200:
|
||
return Connection(is_connected=True)
|
||
else:
|
||
return Connection(is_connected=False, error=response.json())
|
||
except JiraGetCloudIDNoResourcesError as no_resources_error:
|
||
logger.error(
|
||
f"{no_resources_error.__class__.__name__}[{no_resources_error.__traceback__.tb_lineno}]: {no_resources_error}"
|
||
)
|
||
if raise_on_exception:
|
||
raise no_resources_error
|
||
return Connection(error=no_resources_error)
|
||
except JiraGetCloudIDResponseError as response_error:
|
||
logger.error(
|
||
f"{response_error.__class__.__name__}[{response_error.__traceback__.tb_lineno}]: {response_error}"
|
||
)
|
||
if raise_on_exception:
|
||
raise response_error
|
||
return Connection(error=response_error)
|
||
except JiraGetCloudIDError as cloud_id_error:
|
||
logger.error(
|
||
f"{cloud_id_error.__class__.__name__}[{cloud_id_error.__traceback__.tb_lineno}]: {cloud_id_error}"
|
||
)
|
||
if raise_on_exception:
|
||
raise cloud_id_error
|
||
return Connection(error=cloud_id_error)
|
||
except JiraAuthenticationError as auth_error:
|
||
logger.error(
|
||
f"{auth_error.__class__.__name__}[{auth_error.__traceback__.tb_lineno}]: {auth_error}"
|
||
)
|
||
if raise_on_exception:
|
||
raise auth_error
|
||
return Connection(error=auth_error)
|
||
except Exception as error:
|
||
logger.error(f"Failed to test connection: {error}")
|
||
if raise_on_exception:
|
||
raise JiraTestConnectionError(
|
||
message="Failed to test connection on the Jira integration",
|
||
file=os.path.basename(__file__),
|
||
)
|
||
return Connection(is_connected=False, error=error)
|
||
```
|
||
|
||
* Send Findings Method:
|
||
|
||
* Add a method to send Prowler findings to the target platform, adhering to its API specifications.
|
||
|
||
#### Method Implementation
|
||
|
||
The following example demonstrates the `send_findings` method for the `Jira` class:
|
||
|
||
```python title="Send findings method"
|
||
def send_findings(
|
||
self,
|
||
findings: list[Finding] = None,
|
||
project_key: str = None,
|
||
issue_type: str = None,
|
||
):
|
||
"""
|
||
Send the findings to Jira
|
||
|
||
Args:
|
||
- findings: The findings to send
|
||
- project_key: The project key
|
||
- issue_type: The issue type
|
||
|
||
Raises:
|
||
- JiraRefreshTokenError: Failed to refresh the access token
|
||
- JiraRefreshTokenResponseError: Failed to refresh the access token, response code did not match 200
|
||
- JiraCreateIssueError: Failed to create an issue in Jira
|
||
- JiraSendFindingsResponseError: Failed to send the findings to Jira
|
||
"""
|
||
try:
|
||
access_token = self.get_access_token()
|
||
|
||
if not access_token:
|
||
raise JiraNoTokenError(
|
||
message="No token was found",
|
||
file=os.path.basename(__file__),
|
||
)
|
||
|
||
projects = self.get_projects()
|
||
|
||
if project_key not in projects:
|
||
logger.error("The project key is invalid")
|
||
raise JiraInvalidProjectKeyError(
|
||
message="The project key is invalid",
|
||
file=os.path.basename(__file__),
|
||
)
|
||
|
||
available_issue_types = self.get_available_issue_types(project_key)
|
||
|
||
if issue_type not in available_issue_types:
|
||
logger.error("The issue type is invalid")
|
||
raise JiraInvalidIssueTypeError(
|
||
message="The issue type is invalid", file=os.path.basename(__file__)
|
||
)
|
||
headers = {
|
||
"Authorization": f"Bearer {access_token}",
|
||
"Content-Type": "application/json",
|
||
}
|
||
|
||
for finding in findings:
|
||
status_color = self.get_color_from_status(finding.status.value)
|
||
adf_description = self.get_adf_description(
|
||
check_id=finding.metadata.CheckID,
|
||
check_title=finding.metadata.CheckTitle,
|
||
severity=finding.metadata.Severity.value.upper(),
|
||
status=finding.status.value,
|
||
status_color=status_color,
|
||
status_extended=finding.status_extended,
|
||
provider=finding.metadata.Provider,
|
||
region=finding.region,
|
||
resource_uid=finding.resource_uid,
|
||
resource_name=finding.resource_name,
|
||
risk=finding.metadata.Risk,
|
||
recommendation_text=finding.metadata.Remediation.Recommendation.Text,
|
||
recommendation_url=finding.metadata.Remediation.Recommendation.Url,
|
||
)
|
||
payload = {
|
||
"fields": {
|
||
"project": {"key": project_key},
|
||
"summary": f"[Prowler] {finding.metadata.Severity.value.upper()} - {finding.metadata.CheckID} - {finding.resource_uid}",
|
||
"description": adf_description,
|
||
"issuetype": {"name": issue_type},
|
||
}
|
||
}
|
||
|
||
response = requests.post(
|
||
f"https://api.atlassian.com/ex/jira/{self.cloud_id}/rest/api/3/issue",
|
||
json=payload,
|
||
headers=headers,
|
||
)
|
||
|
||
if response.status_code != 201:
|
||
response_error = f"Failed to send finding: {response.status_code} - {response.json()}"
|
||
logger.warning(response_error)
|
||
raise JiraSendFindingsResponseError(
|
||
message=response_error, file=os.path.basename(__file__)
|
||
)
|
||
else:
|
||
logger.info(f"Finding sent successfully: {response.json()}")
|
||
except JiraRefreshTokenError as refresh_error:
|
||
raise refresh_error
|
||
except JiraRefreshTokenResponseError as response_error:
|
||
raise response_error
|
||
except Exception as e:
|
||
logger.error(f"Failed to send findings: {e}")
|
||
raise JiraCreateIssueError(
|
||
message="Failed to create an issue in Jira",
|
||
file=os.path.basename(__file__),
|
||
)
|
||
```
|
||
|
||
### Testing the Integration
|
||
|
||
* Conduct integration testing in a controlled environment to validate expected behavior. Ensure the following:
|
||
|
||
* Transmission Accuracy – Verify that Prowler findings are correctly sent and processed by the target platform.
|
||
* Error Handling – Simulate edge cases to assess robustness and failure recovery mechanisms.
|
||
|
||
### Documentation
|
||
|
||
* Ensure the following elements are included:
|
||
|
||
* Setup Instructions – List all necessary dependencies and installation steps.
|
||
* Configuration Details – Specify required environment variables, authentication steps, etc.
|
||
* Example Use Cases – Provide practical scenarios demonstrating functionality.
|
||
* Troubleshooting Guide – Document common issues and resolution steps.
|
||
* Comprehensive and clear documentation improves maintainability and simplifies onboarding.
|