Skip to content

Commit 7b45755

Browse files
authored
Version 2.0.1 (#70)
* Add 404, 403 error handling and warning to CTR_ENTITIES_LIMIT variable * Add Jenkinsfile * Fix autotest * Updated test data into test_judgement.py and test_verdict.py files. * Alpine & Python version update * Update tips
1 parent dddd323 commit 7b45755

10 files changed

+70
-49
lines changed

Dockerfile

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1-
FROM alpine:3.13
1+
FROM alpine:3.14
22
LABEL maintainer="Ian Redden <[email protected]>"
33

44
# install packages we need
5-
RUN apk update && apk add --no-cache musl-dev openssl-dev gcc python3 py3-configobj python3-dev supervisor git libffi-dev uwsgi-python3 uwsgi-http jq nano syslog-ng uwsgi-syslog py3-pip
5+
RUN apk update && apk add --no-cache musl-dev openssl-dev gcc py3-configobj \
6+
supervisor git libffi-dev uwsgi-python3 uwsgi-http jq syslog-ng uwsgi-syslog \
7+
py3-pip python3-dev
68

79
# do the Python dependencies
810
ADD code /app

Jenkinsfile

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
@Library('softserve-jenkins-library@main') _
2+
3+
startPipeline()

README.md

+2
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ curl http://localhost:9090
7272

7373
## Implementation Details
7474

75+
This application was developed and tested under Python version 3.9.
76+
7577
### Implemented Relay Endpoints
7678

7779
- `POST /health`

code/api/utils.py

+46-32
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
1-
import requests
2-
import jwt
31
import json
4-
from jwt import InvalidSignatureError, DecodeError, InvalidAudienceError
5-
from flask import request, current_app, jsonify, g
6-
from requests.exceptions import SSLError, ConnectionError, InvalidURL
72
from http import HTTPStatus
3+
from json.decoder import JSONDecodeError
4+
5+
import jwt
6+
import requests
7+
from flask import request, current_app, jsonify, g
8+
from jwt import InvalidSignatureError, DecodeError, InvalidAudienceError
9+
from requests.exceptions import (
10+
SSLError,
11+
ConnectionError,
12+
InvalidURL,
13+
HTTPError
14+
)
815

916
from api.errors import (
1017
AuthorizationError,
@@ -14,6 +21,20 @@
1421
UnexpectedPulsediveError
1522
)
1623

24+
NO_AUTH_HEADER = 'Authorization header is missing'
25+
WRONG_AUTH_TYPE = 'Wrong authorization type'
26+
WRONG_PAYLOAD_STRUCTURE = 'Wrong JWT payload structure'
27+
WRONG_JWT_STRUCTURE = 'Wrong JWT structure'
28+
WRONG_AUDIENCE = 'Wrong configuration-token-audience'
29+
KID_NOT_FOUND = 'kid from JWT header not found in API response'
30+
WRONG_KEY = ('Failed to decode JWT with provided key. '
31+
'Make sure domain in custom_jwks_host '
32+
'corresponds to your SecureX instance region.')
33+
JWKS_HOST_MISSING = ('jwks_host is missing in JWT payload. Make sure '
34+
'custom_jwks_host field is present in module_type')
35+
WRONG_JWKS_HOST = ('Wrong jwks_host in JWT payload. Make sure domain follows '
36+
'the visibility.<region>.cisco.com structure')
37+
1738

1839
def set_ctr_entities_limit(payload):
1940
try:
@@ -25,16 +46,15 @@ def set_ctr_entities_limit(payload):
2546

2647

2748
def get_public_key(jwks_host, token):
28-
expected_errors = {
29-
ConnectionError: 'Wrong jwks_host in JWT payload. '
30-
'Make sure domain follows the '
31-
'visibility.<region>.cisco.com structure',
32-
InvalidURL: 'Wrong jwks_host in JWT payload. '
33-
'Make sure domain follows the '
34-
'visibility.<region>.cisco.com structure',
35-
}
49+
expected_errors = (
50+
ConnectionError,
51+
InvalidURL,
52+
JSONDecodeError,
53+
HTTPError,
54+
)
3655
try:
3756
response = requests.get(f"https://{jwks_host}/.well-known/jwks")
57+
response.raise_for_status()
3858
jwks = response.json()
3959

4060
public_keys = {}
@@ -45,9 +65,8 @@ def get_public_key(jwks_host, token):
4565
)
4666
kid = jwt.get_unverified_header(token)['kid']
4767
return public_keys.get(kid)
48-
except tuple(expected_errors) as error:
49-
message = expected_errors[error.__class__]
50-
raise AuthorizationError(message)
68+
except expected_errors:
69+
raise AuthorizationError(WRONG_JWKS_HOST)
5170

5271

5372
def get_jwt():
@@ -57,24 +76,19 @@ def get_jwt():
5776
"""
5877

5978
expected_errors = {
60-
KeyError: 'Wrong JWT payload structure',
61-
AssertionError: 'jwks_host is missing in JWT payload. '
62-
'Make sure custom_jwks_host field '
63-
'is present in module_type',
64-
InvalidSignatureError: 'Failed to decode JWT with provided key. '
65-
'Make sure domain in custom_jwks_host '
66-
'corresponds to your SecureX instance region.',
67-
DecodeError: 'Wrong JWT structure',
68-
InvalidAudienceError: 'Wrong configuration-token-audience',
69-
TypeError: 'kid from JWT header not found in API response'
79+
KeyError: WRONG_PAYLOAD_STRUCTURE,
80+
AssertionError: JWKS_HOST_MISSING,
81+
InvalidSignatureError: WRONG_KEY,
82+
DecodeError: WRONG_JWT_STRUCTURE,
83+
InvalidAudienceError: WRONG_AUDIENCE,
84+
TypeError: KID_NOT_FOUND,
7085
}
7186

7287
token = get_auth_token()
7388
try:
74-
jwks_host = jwt.decode(
75-
token, options={'verify_signature': False}
76-
).get('jwks_host')
77-
assert jwks_host
89+
jwks_payload = jwt.decode(token, options={'verify_signature': False})
90+
assert 'jwks_host' in jwks_payload
91+
jwks_host = jwks_payload.get('jwks_host')
7892
key = get_public_key(jwks_host, token)
7993
aud = request.url_root
8094
payload = jwt.decode(
@@ -93,8 +107,8 @@ def get_auth_token():
93107
"""
94108

95109
expected_errors = {
96-
KeyError: 'Authorization header is missing',
97-
AssertionError: 'Wrong authorization type'
110+
KeyError: NO_AUTH_HEADER,
111+
AssertionError: WRONG_AUTH_TYPE
98112
}
99113

100114
try:

code/container_settings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"VERSION": "2.0.0",
2+
"VERSION": "2.0.1",
33
"NAME": "Pulsedive"
44
}

code/requirements.txt

+6-6
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
Flask==1.1.2
2-
marshmallow==3.11.1
1+
Flask==2.0.1
2+
marshmallow==3.12.1
33
requests==2.25.1
44
cryptography==3.3.2
5-
pyjwt[crypto]==2.0.1
6-
flake8==3.9.0
7-
coverage==5.2.1
8-
pytest==6.2.2
5+
pyjwt[crypto]==2.1.0
6+
flake8==3.9.2
7+
coverage==5.5
8+
pytest==6.2.4

code/tests/functional/tests/test_judgement.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@
1010

1111
@pytest.mark.parametrize(
1212
'observable,observable_type,disposition_name,disposition',
13-
(('50.63.202.50', 'ip', 'Clean', 1),
14-
('1.0.3.4', 'ip', 'Suspicious', 3),
13+
(('110.174.93.63', 'ip', 'Clean', 1),
14+
('5.9.56.12', 'ip', 'Suspicious', 3),
1515
('0-100-disc.foxypool.cf', 'domain', 'Malicious', 2),
1616
('https://www.youtube.com/', 'url', 'Unknown', 5))
1717
)

code/tests/functional/tests/test_sightings.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
(
1515
('url', 'http://51jianli.cn/images'),
1616
('ip', '1.1.1.1'),
17-
('domain', 'xcj10.me'),
17+
('domain', 'baidu.com'),
1818
)
1919
)
2020
def test_positive_sighting(module_headers, observable, observable_type):
@@ -65,7 +65,7 @@ def test_positive_sighting(module_headers, observable, observable_type):
6565
assert sighting['observables'] == observables
6666
if sighting['description'] == 'Active DNS':
6767
for relation in sighting['relations']:
68-
assert relation['origin'] == f'{MODULE_NAME} Enrichment Module'
68+
assert relation['origin'] == f'{SOURCE} Enrichment Module'
6969
assert relation['relation'] == 'Resolved_To'
7070

7171
if observable_type == 'domain':

code/tests/functional/tests/test_verdict.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@
66

77
@pytest.mark.parametrize(
88
'observable,observable_type,disposition_name,disposition',
9-
(('50.63.202.50', 'ip', 'Clean', 1),
9+
(('110.174.93.63', 'ip', 'Clean', 1),
1010
('0-100-disc.foxypool.cf', 'domain', 'Malicious', 2),
11-
('1.0.3.4', 'ip', 'Suspicious', 3),
11+
('5.9.56.12', 'ip', 'Suspicious', 3),
1212
('98.159.110.20', 'ip', 'Unknown', 5))
1313
)
1414
def test_positive_verdict(module_headers, observable, observable_type,

module_type.json.sample

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"default_name": "Pulsedive",
44
"short_description": "Pulsedive threat intelligence enriches any domain, URL, or IP. Scan new indicators, pivot to search on any data point, and investigate threats.",
55
"description": "Pulsedive threat intelligence offers a community platform that scans, enriches, and scores millions of indicators of compromise, setting the foundation for powerful threat investigation and research capabilities. Register for a free account and API key to access the intelligence sourced and contextualized from dozens of feeds and submitted by users all over the world.\n\nAdditionally, leverage Pulsedive to run passive or active scans on any indicator, investigate shared threat properties and attributes, and pull related threat news/summaries from the web.\n## Snapshot of Pulsedive's API\n\n### Retrieve Indicator Data\n- Risk scores and risk factors\n- Registration timeline\n- Source feeds and comments\n- Associated threats\n- Ports and protocols\n- Web technologies\n- WHOIS registration\n- Location data\n- DNS records\n- Query strings\n- HTTP headers\n- SSL certificate metadata\n- Cookies\n- Meta tags\n- Mail servers\n- Redirects\n- Related domains and urls\n\n### Retrieve Threat Data\n- Related news\n- Comments\n- Risky properties\n- Source feeds\n- Indicators\n\n### Retrieve Feed Data\n- Name and organization\n- Threats\n- Indicators\n\n### Explore (Search) Our Database\n- Create queries using almost any data point(s) to pivot across indicators, threats, or feeds.\n\n### Scan Indicators\n- Perform passive or active scans of hosts to retrieve live data on-demand.",
6-
"tips": "When configuring this integration, you must first gather some information from your Pulsedive account.\n\n1. Log into Pulsedive, click **ACCOUNT**\n2. Copy the **API KEY** into a file, or leave the tab open\n\n3. Complete the **Add New Pulsedive Integration Module** form:\n - **Module Name** - Leave the default name or enter a name that is meaningful to you\n - **API Key** - Enter the Pulsedive API Key\n - **Entities Limit** - Enter the limit that restricts the maximum number of CTIM entities of each type returned in a single response per each requested observable. Must be a positive integer. Defaults to 100 (if unset or incorrect)\n4. Click **Save** to complete the Pulsedive module configuration.",
6+
"tips": "When configuring Pulsedive integration, you must obtain the API key from your Pulsedive account and then add the Pulsedive integration module in SecureX.\n\n1. Log in to Pulsedive and click **ACCOUNT**.\n2. Copy the **API KEY** into a file or leave the tab open.\n3. In SecureX, complete the **Add New Pulsedive Integration Module** form:\n - **Integration Module Name** - Leave the default name or enter a name that is meaningful to you.\n - **API Key** - Paste the copied API key from Pulsedive into this field.\n - **Entities Limit** - Specify the maximum number of indicators and sightings in a single response, per requested observable (must be a positive value). We recommend that you enter a limit in the range of 50 to 1000. The default is 100 entities.\n\n4. Click **Save** to complete the Pulsedive integration module configuration.",
77
"external_references": [
88
{
99
"label": "About",
@@ -26,7 +26,7 @@
2626
"key": "custom_CTR_ENTITIES_LIMIT",
2727
"type": "integer",
2828
"label": "Entities Limit",
29-
"tooltip": "Restricts the maximum number of `Indicators` and `Sightings`",
29+
"tooltip": "Restricts the maximum number of `Indicators` and `Sightings`. Please note that the number over 100 might lead to data inconsistency.",
3030
"required": false
3131
}
3232
],

0 commit comments

Comments
 (0)