Skip to content

Commit b778778

Browse files
authored
Merge branch 'main' into cleanup_mitre_actors_and_techniques
2 parents b09fc94 + 8cc3dbe commit b778778

File tree

4 files changed

+112
-32
lines changed

4 files changed

+112
-32
lines changed

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/contentctl.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import pathlib
2+
import random
23
import sys
34
import traceback
45
import warnings
6+
from dataclasses import dataclass
57

68
import tyro
79

@@ -155,6 +157,35 @@ def test_common_func(config: test_common):
155157
"""
156158

157159

160+
def get_random_compliment():
161+
compliments = [
162+
"Your detection rules are like a zero-day shield! 🛡️",
163+
"You catch threats like it's child's play! 🎯",
164+
"Your correlation rules are pure genius! 🧠",
165+
"Threat actors fear your detection engineering! ⚔️",
166+
"You're the SOC's secret weapon! 🦾",
167+
"Your false positive rate is impressively low! 📊",
168+
"Malware trembles at your detection logic! 🦠",
169+
"You're the threat hunter extraordinaire! 🔍",
170+
"Your MITRE mappings are a work of art! 🎨",
171+
"APTs have nightmares about your detections! 👻",
172+
"Your content testing is bulletproof! 🎯",
173+
"You're the detection engineering MVP! 🏆",
174+
]
175+
return random.choice(compliments)
176+
177+
178+
def recognize_func():
179+
print(get_random_compliment())
180+
181+
182+
@dataclass
183+
class RecognizeCommand:
184+
"""Dummy subcommand for 'recognize' with no parameters."""
185+
186+
pass
187+
188+
158189
def main():
159190
print(CONTENTCTL_5_WARNING)
160191
try:
@@ -210,6 +241,7 @@ def main():
210241
"test_servers": test_servers.model_construct(**t.__dict__),
211242
"release_notes": release_notes.model_construct(**config_obj),
212243
"deploy_acs": deploy_acs.model_construct(**t.__dict__),
244+
"recognize": RecognizeCommand(),
213245
}
214246
)
215247

@@ -240,6 +272,8 @@ def main():
240272
deploy_acs_func(updated_config)
241273
elif type(config) is test or type(config) is test_servers:
242274
test_common_func(config)
275+
elif type(config) is RecognizeCommand:
276+
recognize_func()
243277
else:
244278
raise Exception(f"Unknown command line type '{type(config).__name__}'")
245279
except FileNotFoundError as e:

contentctl/objects/config.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -425,7 +425,6 @@ class inspect(build):
425425
"enforcement (defaults to the latest release of the app published on Splunkbase)."
426426
),
427427
)
428-
stack_type: StackType = Field(description="The type of your Splunk Cloud Stack")
429428

430429
@field_validator("enrichments", mode="after")
431430
@classmethod
@@ -496,6 +495,9 @@ class new(Config_Base):
496495

497496
class deploy_acs(inspect):
498497
model_config = ConfigDict(validate_default=False, arbitrary_types_allowed=True)
498+
499+
stack_type: StackType = Field(description="The type of your Splunk Cloud Stack")
500+
499501
# ignore linter error
500502
splunk_cloud_jwt_token: str = Field(
501503
exclude=True,

contentctl/objects/data_source.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from __future__ import annotations
2-
from typing import Optional, Any
3-
from pydantic import Field, HttpUrl, model_serializer, BaseModel
2+
3+
from typing import Any, Optional
4+
5+
from pydantic import BaseModel, Field, HttpUrl, model_serializer
6+
47
from contentctl.objects.security_content_object import SecurityContentObject
58

69

@@ -20,6 +23,7 @@ class DataSource(SecurityContentObject):
2023
field_mappings: None | list = None
2124
convert_to_log_source: None | list = None
2225
example_log: None | str = None
26+
output_fields: list[str] = []
2327

2428
@model_serializer
2529
def serialize_model(self):

0 commit comments

Comments
 (0)