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
1 change: 1 addition & 0 deletions .git-blame-ignore-revs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ff87bcaf1741e8ecf15cb8d401438592dfef3ba7 # Mass reformat with adoption of ruff
1 change: 0 additions & 1 deletion .github/workflows/test_against_escu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ jobs:
with:
path: security_content
repository: splunk/security_content
ref: rba_migration

#Install the given version of Python we will test against
- name: Install Required Python Version
Expand Down
2 changes: 1 addition & 1 deletion contentctl/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = '0.1.0'
__version__ = "0.1.0"
143 changes: 88 additions & 55 deletions contentctl/actions/build.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@
import sys
import shutil
import os

from dataclasses import dataclass

from contentctl.objects.enums import SecurityContentType
from contentctl.input.director import Director, DirectorOutputDto
from contentctl.input.director import DirectorOutputDto
from contentctl.output.conf_output import ConfOutput
from contentctl.output.conf_writer import ConfWriter
from contentctl.output.api_json_output import ApiJsonOutput
Expand All @@ -18,86 +15,122 @@

from contentctl.objects.config import build


@dataclass(frozen=True)
class BuildInputDto:
director_output_dto: DirectorOutputDto
config:build
config: build


class Build:



def execute(self, input_dto: BuildInputDto) -> DirectorOutputDto:
if input_dto.config.build_app:

updated_conf_files:set[pathlib.Path] = set()
updated_conf_files: set[pathlib.Path] = set()
conf_output = ConfOutput(input_dto.config)


# Construct a path to a YML that does not actually exist.
# We mock this "fake" path since the YML does not exist.
# This ensures the checking for the existence of the CSV is correct
data_sources_fake_yml_path = input_dto.config.getPackageDirectoryPath() / "lookups" / "data_sources.yml"
data_sources_fake_yml_path = (
input_dto.config.getPackageDirectoryPath()
/ "lookups"
/ "data_sources.yml"
)

# Construct a special lookup whose CSV is created at runtime and
# written directly into the lookups folder. We will delete this after a build,
# written directly into the lookups folder. We will delete this after a build,
# assuming that it is successful.
data_sources_lookup_csv_path = input_dto.config.getPackageDirectoryPath() / "lookups" / "data_sources.csv"



DataSourceWriter.writeDataSourceCsv(input_dto.director_output_dto.data_sources, data_sources_lookup_csv_path)
input_dto.director_output_dto.addContentToDictMappings(CSVLookup.model_construct(name="data_sources",
id=uuid.UUID("b45c1403-6e09-47b0-824f-cf6e44f15ac8"),
version=1,
author=input_dto.config.app.author_name,
date = datetime.date.today(),
description= "A lookup file that will contain the data source objects for detections.",
lookup_type=Lookup_Type.csv,
file_path=data_sources_fake_yml_path))
data_sources_lookup_csv_path = (
input_dto.config.getPackageDirectoryPath()
/ "lookups"
/ "data_sources.csv"
)

DataSourceWriter.writeDataSourceCsv(
input_dto.director_output_dto.data_sources, data_sources_lookup_csv_path
)
input_dto.director_output_dto.addContentToDictMappings(
CSVLookup.model_construct(
name="data_sources",
id=uuid.UUID("b45c1403-6e09-47b0-824f-cf6e44f15ac8"),
version=1,
author=input_dto.config.app.author_name,
date=datetime.date.today(),
description="A lookup file that will contain the data source objects for detections.",
lookup_type=Lookup_Type.csv,
file_path=data_sources_fake_yml_path,
)
)
updated_conf_files.update(conf_output.writeHeaders())
updated_conf_files.update(conf_output.writeLookups(input_dto.director_output_dto.lookups))
updated_conf_files.update(conf_output.writeDetections(input_dto.director_output_dto.detections))
updated_conf_files.update(conf_output.writeStories(input_dto.director_output_dto.stories))
updated_conf_files.update(conf_output.writeBaselines(input_dto.director_output_dto.baselines))
updated_conf_files.update(conf_output.writeInvestigations(input_dto.director_output_dto.investigations))
updated_conf_files.update(conf_output.writeMacros(input_dto.director_output_dto.macros))
updated_conf_files.update(conf_output.writeDashboards(input_dto.director_output_dto.dashboards))
updated_conf_files.update(
conf_output.writeLookups(input_dto.director_output_dto.lookups)
)
updated_conf_files.update(
conf_output.writeDetections(input_dto.director_output_dto.detections)
)
updated_conf_files.update(
conf_output.writeStories(input_dto.director_output_dto.stories)
)
updated_conf_files.update(
conf_output.writeBaselines(input_dto.director_output_dto.baselines)
)
updated_conf_files.update(
conf_output.writeInvestigations(
input_dto.director_output_dto.investigations
)
)
updated_conf_files.update(
conf_output.writeMacros(input_dto.director_output_dto.macros)
)
updated_conf_files.update(
conf_output.writeDashboards(input_dto.director_output_dto.dashboards)
)
updated_conf_files.update(conf_output.writeMiscellaneousAppFiles())




#Ensure that the conf file we just generated/update is syntactically valid
# Ensure that the conf file we just generated/update is syntactically valid
for conf_file in updated_conf_files:
ConfWriter.validateConfFile(conf_file)
ConfWriter.validateConfFile(conf_file)

conf_output.packageApp()

print(f"Build of '{input_dto.config.app.title}' APP successful to {input_dto.config.getPackageFilePath()}")

print(
f"Build of '{input_dto.config.app.title}' APP successful to {input_dto.config.getPackageFilePath()}"
)

if input_dto.config.build_api:
if input_dto.config.build_api:
shutil.rmtree(input_dto.config.getAPIPath(), ignore_errors=True)
input_dto.config.getAPIPath().mkdir(parents=True)
api_json_output = ApiJsonOutput(input_dto.config.getAPIPath(), input_dto.config.app.label)
api_json_output = ApiJsonOutput(
input_dto.config.getAPIPath(), input_dto.config.app.label
)
api_json_output.writeDetections(input_dto.director_output_dto.detections)
api_json_output.writeStories(input_dto.director_output_dto.stories)
api_json_output.writeBaselines(input_dto.director_output_dto.baselines)
api_json_output.writeInvestigations(input_dto.director_output_dto.investigations)
api_json_output.writeInvestigations(
input_dto.director_output_dto.investigations
)
api_json_output.writeLookups(input_dto.director_output_dto.lookups)
api_json_output.writeMacros(input_dto.director_output_dto.macros)
api_json_output.writeDeployments(input_dto.director_output_dto.deployments)


#create version file for sse api
version_file = input_dto.config.getAPIPath()/"version.json"
utc_time = datetime.datetime.now(datetime.timezone.utc).replace(microsecond=0,tzinfo=None).isoformat()
version_dict = {"version":{"name":f"v{input_dto.config.app.version}","published_at": f"{utc_time}Z" }}
with open(version_file,"w") as version_f:
json.dump(version_dict,version_f)

print(f"Build of '{input_dto.config.app.title}' API successful to {input_dto.config.getAPIPath()}")

return input_dto.director_output_dto
# create version file for sse api
version_file = input_dto.config.getAPIPath() / "version.json"
utc_time = (
datetime.datetime.now(datetime.timezone.utc)
.replace(microsecond=0, tzinfo=None)
.isoformat()
)
version_dict = {
"version": {
"name": f"v{input_dto.config.app.version}",
"published_at": f"{utc_time}Z",
}
}
with open(version_file, "w") as version_f:
json.dump(version_dict, version_f)

print(
f"Build of '{input_dto.config.app.title}' API successful to {input_dto.config.getAPIPath()}"
)

return input_dto.director_output_dto
53 changes: 29 additions & 24 deletions contentctl/actions/deploy_acs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,52 +4,57 @@


class Deploy:
def execute(self, config: deploy_acs, appinspect_token:str) -> None:

#The following common headers are used by both Clasic and Victoria
def execute(self, config: deploy_acs, appinspect_token: str) -> None:
# The following common headers are used by both Clasic and Victoria
headers = {
'Authorization': f'Bearer {config.splunk_cloud_jwt_token}',
'ACS-Legal-Ack': 'Y'
"Authorization": f"Bearer {config.splunk_cloud_jwt_token}",
"ACS-Legal-Ack": "Y",
}
try:

with open(config.getPackageFilePath(include_version=False),'rb') as app_data:
#request_data = app_data.read()
with open(
config.getPackageFilePath(include_version=False), "rb"
) as app_data:
# request_data = app_data.read()
if config.stack_type == StackType.classic:
# Classic instead uses a form to store token and package
# https://docs.splunk.com/Documentation/SplunkCloud/9.1.2308/Config/ManageApps#Manage_private_apps_using_the_ACS_API_on_Classic_Experience
address = f"https://admin.splunk.com/{config.splunk_cloud_stack}/adminconfig/v2/apps"

form_data = {
'token': (None, appinspect_token),
'package': app_data
}
res = post(address, headers=headers, files = form_data)

form_data = {"token": (None, appinspect_token), "package": app_data}
res = post(address, headers=headers, files=form_data)
elif config.stack_type == StackType.victoria:
# Victoria uses the X-Splunk-Authorization Header
# It also uses --data-binary for the app content
# https://docs.splunk.com/Documentation/SplunkCloud/9.1.2308/Config/ManageApps#Manage_private_apps_using_the_ACS_API_on_Victoria_Experience
headers.update({'X-Splunk-Authorization': appinspect_token})
headers.update({"X-Splunk-Authorization": appinspect_token})
address = f"https://admin.splunk.com/{config.splunk_cloud_stack}/adminconfig/v2/apps/victoria"
res = post(address, headers=headers, data=app_data.read())
else:
raise Exception(f"Unsupported stack type: '{config.stack_type}'")
except Exception as e:
raise Exception(f"Error installing to stack '{config.splunk_cloud_stack}' (stack_type='{config.stack_type}') via ACS:\n{str(e)}")

raise Exception(
f"Error installing to stack '{config.splunk_cloud_stack}' (stack_type='{config.stack_type}') via ACS:\n{str(e)}"
)

try:
# Request went through and completed, but may have returned a non-successful error code.
# This likely includes a more verbose response describing the error
res.raise_for_status()
print(res.json())
except Exception as e:
except Exception:
try:
error_text = res.json()
except Exception as e:
except Exception:
error_text = "No error text - request failed"
formatted_error_text = pprint.pformat(error_text)
print("While this may not be the cause of your error, ensure that the uid and appid of your Private App does not exist in Splunkbase\n"
"ACS cannot deploy and app with the same uid or appid as one that exists in Splunkbase.")
raise Exception(f"Error installing to stack '{config.splunk_cloud_stack}' (stack_type='{config.stack_type}') via ACS:\n{formatted_error_text}")

print(f"'{config.getPackageFilePath(include_version=False)}' successfully installed to stack '{config.splunk_cloud_stack}' (stack_type='{config.stack_type}') via ACS!")
print(
"While this may not be the cause of your error, ensure that the uid and appid of your Private App does not exist in Splunkbase\n"
"ACS cannot deploy and app with the same uid or appid as one that exists in Splunkbase."
)
raise Exception(
f"Error installing to stack '{config.splunk_cloud_stack}' (stack_type='{config.stack_type}') via ACS:\n{formatted_error_text}"
)

print(
f"'{config.getPackageFilePath(include_version=False)}' successfully installed to stack '{config.splunk_cloud_stack}' (stack_type='{config.stack_type}') via ACS!"
)
Loading
Loading