Skip to content

Commit d79055a

Browse files
authored
Merge branch 'main' into snapattack_datasource_enrichments
2 parents 2d124d6 + 8cc3dbe commit d79055a

32 files changed

+616
-518
lines changed

.DS_Store

-6 KB
Binary file not shown.

contentctl/actions/detection_testing/infrastructures/DetectionTestingInfrastructure.py

Lines changed: 46 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,46 @@
1-
import time
2-
import uuid
31
import abc
4-
import os.path
52
import configparser
6-
import json
73
import datetime
8-
import tqdm # type: ignore
4+
import json
5+
import os.path
96
import pathlib
10-
from tempfile import TemporaryDirectory, mktemp
7+
import time
8+
import urllib.parse
9+
import uuid
10+
from shutil import copyfile
1111
from ssl import SSLEOFError, SSLZeroReturnError
1212
from sys import stdout
13-
from shutil import copyfile
14-
from typing import Union, Optional
13+
from tempfile import TemporaryDirectory, mktemp
14+
from typing import Optional, Union
1515

16-
from pydantic import ConfigDict, BaseModel, PrivateAttr, Field, dataclasses
1716
import requests # type: ignore
1817
import splunklib.client as client # type: ignore
18+
import splunklib.results
19+
import tqdm # type: ignore
20+
from pydantic import BaseModel, ConfigDict, Field, PrivateAttr, dataclasses
1921
from splunklib.binding import HTTPError # type: ignore
2022
from splunklib.results import JSONResultsReader, Message # type: ignore
21-
import splunklib.results
2223
from urllib3 import disable_warnings
23-
import urllib.parse
2424

25-
from contentctl.objects.config import test_common, Infrastructure
26-
from contentctl.objects.enums import PostTestBehavior, AnalyticsType
27-
from contentctl.objects.detection import Detection
28-
from contentctl.objects.base_test import BaseTest
29-
from contentctl.objects.unit_test import UnitTest
30-
from contentctl.objects.integration_test import IntegrationTest
31-
from contentctl.objects.test_attack_data import TestAttackData
32-
from contentctl.objects.unit_test_result import UnitTestResult
33-
from contentctl.objects.integration_test_result import IntegrationTestResult
34-
from contentctl.objects.test_group import TestGroup
35-
from contentctl.objects.base_test_result import TestResultStatus
36-
from contentctl.objects.correlation_search import CorrelationSearch, PbarData
37-
from contentctl.helper.utils import Utils
3825
from contentctl.actions.detection_testing.progress_bar import (
39-
format_pbar_string,
40-
TestReportingType,
4126
FinalTestingStates,
4227
TestingStates,
28+
TestReportingType,
29+
format_pbar_string,
4330
)
31+
from contentctl.helper.utils import Utils
32+
from contentctl.objects.base_test import BaseTest
33+
from contentctl.objects.base_test_result import TestResultStatus
34+
from contentctl.objects.config import Infrastructure, test_common
35+
from contentctl.objects.correlation_search import CorrelationSearch, PbarData
36+
from contentctl.objects.detection import Detection
37+
from contentctl.objects.enums import AnalyticsType, PostTestBehavior
38+
from contentctl.objects.integration_test import IntegrationTest
39+
from contentctl.objects.integration_test_result import IntegrationTestResult
40+
from contentctl.objects.test_attack_data import TestAttackData
41+
from contentctl.objects.test_group import TestGroup
42+
from contentctl.objects.unit_test import UnitTest
43+
from contentctl.objects.unit_test_result import UnitTestResult
4444

4545

4646
class SetupTestGroupResults(BaseModel):
@@ -1109,17 +1109,25 @@ def retry_search_until_timeout(
11091109
job = self.get_conn().search(query=search, **kwargs)
11101110
results = JSONResultsReader(job.results(output_mode="json"))
11111111

1112-
# TODO (cmcginley): @ljstella you're removing this ultimately, right?
1113-
# Consolidate a set of the distinct observable field names
1114-
observable_fields_set = set(
1115-
[o.name for o in detection.tags.observable]
1116-
) # keeping this around for later
1117-
risk_object_fields_set = set(
1118-
[o.name for o in detection.tags.observable if "Victim" in o.role]
1119-
) # just the "Risk Objects"
1120-
threat_object_fields_set = set(
1121-
[o.name for o in detection.tags.observable if "Attacker" in o.role]
1122-
) # just the "threat objects"
1112+
if detection.rba is not None:
1113+
risk_object_fields_set = set(
1114+
[o.field for o in detection.rba.risk_objects]
1115+
) # just the "Risk Objects"
1116+
threat_object_fields_set = set(
1117+
[o.field for o in detection.rba.threat_objects]
1118+
) # just the "threat objects"
1119+
else:
1120+
# For some searches, like Hunting Searches, there should
1121+
# not be any risk or threat objects.
1122+
risk_object_fields_set: set[str] = (
1123+
set()
1124+
) # just the "Risk Objects" (of which there are none)
1125+
threat_object_fields_set: set[str] = (
1126+
set()
1127+
) # just the "threat objects" (of which there are none)
1128+
full_rba_field_set: set[str] = risk_object_fields_set.union(
1129+
threat_object_fields_set
1130+
)
11231131

11241132
# Ensure the search had at least one result
11251133
if int(job.content.get("resultCount", "0")) > 0:
@@ -1164,7 +1172,7 @@ def retry_search_until_timeout(
11641172

11651173
# TODO (cmcginley): @ljstella is this something we're keeping for testing as
11661174
# well?
1167-
for field in observable_fields_set:
1175+
for field in full_rba_field_set:
11681176
if result.get(field, "null") == "null":
11691177
if field in risk_object_fields_set:
11701178
e = Exception(

contentctl/actions/inspect.py

Lines changed: 69 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,45 @@
1-
import sys
2-
from dataclasses import dataclass
3-
import pathlib
4-
import json
51
import datetime
6-
import timeit
2+
import json
3+
import pathlib
4+
import sys
75
import time
6+
import timeit
7+
from dataclasses import dataclass
8+
from io import BufferedReader
89

9-
from requests import Session, post, get
10+
from requests import Session, get, post
1011
from requests.auth import HTTPBasicAuth
1112

1213
from contentctl.objects.config import inspect
13-
from contentctl.objects.savedsearches_conf import SavedsearchesConf
1414
from contentctl.objects.errors import (
15-
MetadataValidationError,
1615
DetectionIDError,
1716
DetectionMissingError,
18-
VersionDecrementedError,
17+
MetadataValidationError,
1918
VersionBumpingError,
19+
VersionDecrementedError,
2020
)
21+
from contentctl.objects.savedsearches_conf import SavedsearchesConf
22+
23+
"""
24+
The following list includes all appinspect tags available from:
25+
https://dev.splunk.com/enterprise/reference/appinspect/appinspecttagreference/
26+
27+
This allows contentctl to be as forward-leaning as possible in catching
28+
any potential issues on the widest variety of stacks.
29+
"""
30+
INCLUDED_TAGS_LIST = [
31+
"aarch64_compatibility",
32+
"ast",
33+
"cloud",
34+
"future",
35+
"manual",
36+
"packaging_standards",
37+
"private_app",
38+
"private_classic",
39+
"private_victoria",
40+
"splunk_appinspect",
41+
]
42+
INCLUDED_TAGS_STRING = ",".join(INCLUDED_TAGS_LIST)
2143

2244

2345
@dataclass(frozen=True)
@@ -28,7 +50,6 @@ class InspectInputDto:
2850
class Inspect:
2951
def execute(self, config: inspect) -> str:
3052
if config.build_app or config.build_api:
31-
self.inspectAppCLI(config)
3253
appinspect_token = self.inspectAppAPI(config)
3354

3455
if config.enable_metadata_validation:
@@ -49,10 +70,6 @@ def inspectAppAPI(self, config: inspect) -> str:
4970
session.auth = HTTPBasicAuth(
5071
config.splunk_api_username, config.splunk_api_password
5172
)
52-
if config.stack_type not in ["victoria", "classic"]:
53-
raise Exception(
54-
f"stack_type MUST be either 'classic' or 'victoria', NOT '{config.stack_type}'"
55-
)
5673

5774
APPINSPECT_API_LOGIN = "https://api.splunk.com/2.0/rest/login/splunk"
5875

@@ -64,10 +81,6 @@ def inspectAppAPI(self, config: inspect) -> str:
6481
APPINSPECT_API_VALIDATION_REQUEST = (
6582
"https://appinspect.splunk.com/v1/app/validate"
6683
)
67-
headers = {
68-
"Authorization": f"bearer {authorization_bearer}",
69-
"Cache-Control": "no-cache",
70-
}
7184

7285
package_path = config.getPackageFilePath(include_version=False)
7386
if not package_path.is_file():
@@ -77,18 +90,43 @@ def inspectAppAPI(self, config: inspect) -> str:
7790
"trying to 'contentctl deploy_acs' the package BEFORE running 'contentctl build'?"
7891
)
7992

80-
files = {
93+
"""
94+
Some documentation on "files" argument for requests.post exists here:
95+
https://docs.python-requests.org/en/latest/api/
96+
The type (None, INCLUDED_TAGS_STRING) is intentional, and the None is important.
97+
In curl syntax, the request we make below is equivalent to
98+
curl -X POST \
99+
-H "Authorization: bearer <TOKEN>" \
100+
-H "Cache-Control: no-cache" \
101+
-F "app_package=@<PATH/APP-PACKAGE>" \
102+
-F "included_tags=cloud" \
103+
--url "https://appinspect.splunk.com/v1/app/validate"
104+
105+
This is confirmed by the great resource:
106+
https://curlconverter.com/
107+
"""
108+
data: dict[str, tuple[None, str] | BufferedReader] = {
81109
"app_package": open(package_path, "rb"),
82-
"included_tags": (None, "cloud"),
110+
"included_tags": (
111+
None,
112+
INCLUDED_TAGS_STRING,
113+
), # tuple with None is intentional here
83114
}
84115

85-
res = post(APPINSPECT_API_VALIDATION_REQUEST, headers=headers, files=files)
116+
headers = {
117+
"Authorization": f"bearer {authorization_bearer}",
118+
"Cache-Control": "no-cache",
119+
}
120+
121+
res = post(APPINSPECT_API_VALIDATION_REQUEST, files=data, headers=headers)
86122

87123
res.raise_for_status()
88124

89125
request_id = res.json().get("request_id", None)
90-
APPINSPECT_API_VALIDATION_STATUS = f"https://appinspect.splunk.com/v1/app/validate/status/{request_id}?included_tags=private_{config.stack_type}"
91-
headers = headers = {"Authorization": f"bearer {authorization_bearer}"}
126+
APPINSPECT_API_VALIDATION_STATUS = (
127+
f"https://appinspect.splunk.com/v1/app/validate/status/{request_id}"
128+
)
129+
92130
startTime = timeit.default_timer()
93131
# the first time, wait for 40 seconds. subsequent times, wait for less.
94132
# this is because appinspect takes some time to return, so there is no sense
@@ -114,7 +152,9 @@ def inspectAppAPI(self, config: inspect) -> str:
114152
raise Exception(f"Error - Unknown Appinspect API status '{status}'")
115153

116154
# We have finished running appinspect, so get the report
117-
APPINSPECT_API_REPORT = f"https://appinspect.splunk.com/v1/app/report/{request_id}?included_tags=private_{config.stack_type}"
155+
APPINSPECT_API_REPORT = (
156+
f"https://appinspect.splunk.com/v1/app/report/{request_id}"
157+
)
118158
# Get human-readable HTML report
119159
headers = headers = {
120160
"Authorization": f"bearer {authorization_bearer}",
@@ -159,14 +199,14 @@ def inspectAppCLI(self, config: inspect) -> None:
159199
"\t - https://dev.splunk.com/enterprise/docs/developapps/testvalidate/appinspect/useappinspectclitool/"
160200
)
161201
from splunk_appinspect.main import (
162-
validate,
163-
MODE_OPTION,
164202
APP_PACKAGE_ARGUMENT,
165-
OUTPUT_FILE_OPTION,
166-
LOG_FILE_OPTION,
167-
INCLUDED_TAGS_OPTION,
168203
EXCLUDED_TAGS_OPTION,
204+
INCLUDED_TAGS_OPTION,
205+
LOG_FILE_OPTION,
206+
MODE_OPTION,
207+
OUTPUT_FILE_OPTION,
169208
TEST_MODE,
209+
validate,
170210
)
171211
except Exception as e:
172212
print(e)

contentctl/actions/new_content.py

Lines changed: 23 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,17 @@
1-
import questionary
2-
from typing import Any
3-
from contentctl.input.new_content_questions import NewContentQuestions
4-
from contentctl.objects.config import new, NewContentType
1+
import pathlib
52
import uuid
63
from datetime import datetime
7-
import pathlib
4+
from typing import Any
5+
6+
import questionary
7+
8+
from contentctl.input.new_content_questions import NewContentQuestions
89
from contentctl.objects.abstract_security_content_objects.security_content_object_abstract import (
910
SecurityContentObject_Abstract,
1011
)
11-
from contentctl.output.yml_writer import YmlWriter
12+
from contentctl.objects.config import NewContentType, new
1213
from contentctl.objects.enums import AssetType
13-
from contentctl.objects.constants import (
14-
SES_OBSERVABLE_TYPE_MAPPING,
15-
SES_OBSERVABLE_ROLE_MAPPING,
16-
)
14+
from contentctl.output.yml_writer import YmlWriter
1715

1816

1917
class NewContent:
@@ -34,6 +32,14 @@ class NewContent:
3432
},
3533
]
3634

35+
DEFAULT_RBA = {
36+
"message": "Risk Message goes here",
37+
"risk_objects": [{"field": "dest", "type": "system", "score": 10}],
38+
"threat_objects": [
39+
{"field": "parent_process_name", "type": "parent_process_name"}
40+
],
41+
}
42+
3743
def buildDetection(self) -> tuple[dict[str, Any], str]:
3844
questions = NewContentQuestions.get_questions_detection()
3945
answers: dict[str, str] = questionary.prompt(
@@ -44,8 +50,8 @@ def buildDetection(self) -> tuple[dict[str, Any], str]:
4450
raise ValueError("User didn't answer one or more questions!")
4551

4652
data_source_field = (
47-
answers["data_source"]
48-
if len(answers["data_source"]) > 0
53+
answers["data_sources"]
54+
if len(answers["data_sources"]) > 0
4955
else [f"{NewContent.UPDATE_PREFIX} zero or more data_sources"]
5056
)
5157
file_name = (
@@ -85,24 +91,13 @@ def buildDetection(self) -> tuple[dict[str, Any], str]:
8591
f"{NewContent.UPDATE_PREFIX} zero or more http references to provide more information about your search"
8692
],
8793
"drilldown_searches": NewContent.DEFAULT_DRILLDOWN_DEF,
94+
"rba": NewContent.DEFAULT_RBA,
8895
"tags": {
8996
"analytic_story": [
9097
f"{NewContent.UPDATE_PREFIX} by providing zero or more analytic stories"
9198
],
9299
"asset_type": f"{NewContent.UPDATE_PREFIX} by providing and asset type from {list(AssetType._value2member_map_)}",
93-
"confidence": f"{NewContent.UPDATE_PREFIX} by providing a value between 1-100",
94-
"impact": f"{NewContent.UPDATE_PREFIX} by providing a value between 1-100",
95-
"message": f"{NewContent.UPDATE_PREFIX} by providing a risk message. Fields in your search results can be referenced using $fieldName$",
96100
"mitre_attack_id": mitre_attack_ids,
97-
"observable": [
98-
{
99-
"name": f"{NewContent.UPDATE_PREFIX} the field name of the observable. This is a field that exists in your search results.",
100-
"type": f"{NewContent.UPDATE_PREFIX} the type of your observable from the list {list(SES_OBSERVABLE_TYPE_MAPPING.keys())}.",
101-
"role": [
102-
f"{NewContent.UPDATE_PREFIX} the role from the list {list(SES_OBSERVABLE_ROLE_MAPPING.keys())}"
103-
],
104-
}
105-
],
106101
"product": [
107102
"Splunk Enterprise",
108103
"Splunk Enterprise Security",
@@ -128,6 +123,9 @@ def buildDetection(self) -> tuple[dict[str, Any], str]:
128123
if answers["detection_type"] not in ["TTP", "Anomaly", "Correlation"]:
129124
del output_file_answers["drilldown_searches"]
130125

126+
if answers["detection_type"] not in ["TTP", "Anomaly"]:
127+
del output_file_answers["rba"]
128+
131129
return output_file_answers, answers["detection_kind"]
132130

133131
def buildStory(self) -> dict[str, Any]:
@@ -142,6 +140,7 @@ def buildStory(self) -> dict[str, Any]:
142140
del answers["story_name"]
143141
answers["id"] = str(uuid.uuid4())
144142
answers["version"] = 1
143+
answers["status"] = "production"
145144
answers["date"] = datetime.today().strftime("%Y-%m-%d")
146145
answers["author"] = answers["story_author"]
147146
del answers["story_author"]

0 commit comments

Comments
 (0)