diff --git a/security/caldera/Makefile b/security/caldera/Makefile index 3544dba8e55a..dff6a25ad848 100644 --- a/security/caldera/Makefile +++ b/security/caldera/Makefile @@ -1,160 +1,163 @@ PORTNAME= caldera DISTVERSION= 5.0.0 -PORTREVISION= 3 +PORTREVISION= 4 CATEGORIES= security python MAINTAINER= acm@FreeBSD.org COMMENT= Automated Adversary Emulation Platform WWW= https://github.com/mitre/caldera CONFLICTS= caldera4 LICENSE= APACHE20 LICENSE_FILE= ${WRKSRC}/LICENSE BUILD_DEPENDS= npm-node18>0:www/npm-node18 RUN_DEPENDS= ${PYTHON_PKGNAMEPREFIX}aiohttp>0:www/py-aiohttp@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}aiohttp-jinja2>0:www/py-aiohttp-jinja2@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}aiohttp-session>0:www/py-aiohttp-session@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}aiohttp-security>0:security/py-aiohttp-security@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}aiohttp-apispec>0:devel/py-aiohttp-apispec@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}Jinja2>0:devel/py-Jinja2@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}yaml>0:devel/py-yaml@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}websockets>0:devel/py-websockets@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}sphinx>0:textproc/py-sphinx@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}docutils>0:textproc/py-docutils@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}sphinx_rtd_theme>0:textproc/py-sphinx_rtd_theme@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}myst-parser>0:textproc/py-myst-parser@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}marshmallow>0:devel/py-marshmallow@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}dirhash>0:security/py-dirhash@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}docker>0:sysutils/py-docker@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}donut-shellcode>0:devel/py-donut-shellcode@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}marshmallow-enum>0:devel/py-marshmallow-enum@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}ldap3>0:net/py-ldap3@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}lxml>0:devel/py-lxml@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}reportlab>0:print/py-reportlab@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}svglib>0:converters/py-svglib@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}markdown>0:textproc/py-markdown@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}dnspython>0:dns/py-dnspython@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}asyncssh>0:security/py-asyncssh@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}aioftp>0:ftp/py-aioftp@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}packaging>0:devel/py-packaging@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}pyautogui>0:x11/py-pyautogui@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}selenium>0:www/py-selenium@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}webdriver_manager>0:www/py-webdriver_manager@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}beautifulsoup>0:www/py-beautifulsoup@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}networkx>0:math/py-networkx@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}numpy>0:math/py-numpy@${PY_FLAVOR} \ upx>0:archivers/upx \ base64>0:converters/base64 \ git>0:devel/git \ bash>0:shells/bash USE_GITHUB= yes GH_ACCOUNT= mitre GH_PROJECT= ${PORTNAME} GH_TUPLE= mitre:access:775181b:access/plugins/access \ mitre:atomic:4f489bb:atomic/plugins/atomic \ mitre:builder:de24b68:builder/plugins/builder \ mitre:compass:85e2ebf:compass/plugins/compass \ mitre:debrief:eba44d5:debrief/plugins/debrief \ mitre:emu:7215768:emu/plugins/emu \ mitre:fieldmanual:cb53f2b:fieldmanual/plugins/fieldmanual \ mitre:gameboard:dacb6ce:gameboard/plugins/gameboard \ mitre:human:0e344ea:human/plugins/human \ mitre:magma:99f0056:magma/plugins/magma \ mitre:manx:a7e83a6:manx/plugins/manx \ mitre:response:8ea6ab5:response/plugins/response \ mitre:sandcat:b408f3f:sandcat/plugins/sandcat \ mitre:ssl:1ae8431:ssl/plugins/ssl \ mitre:stockpile:fe0c5b7:stockpile/plugins/stockpile \ mitre:training:37f998f:training/plugins/training USES= dos2unix go:run nodejs:18,build python USE_PYTHON= cryptography NO_ARCH= yes DOS2UNIX_REGEX= .*\.([yml]) CALDERA_USER= caldera CALDERA_GROUP= caldera USERS= ${CALDERA_USER} GROUPS= ${CALDERA_GROUP} PLIST= ${WRKDIR}/pkg-plist USE_RC_SUBR= ${PORTNAME:S/-/_/} SUB_FILES= pkg-message SUB_LIST= PYTHON_CMD=${PYTHON_CMD} \ WWWDIR=${WWWDIR} OPTIONS_DEFINE= HAPROXY OPTIONS_DEFAULT=HAPROXY HAPROXY_DESC= Support for HTTPS HAPROXY_RUN_DEPENDS=haproxy24>0:net/haproxy24 NPM_BIN= ${LOCALBASE}/bin/npm # to rebuild the deps archive: # 1. set DEV_UPDATE_MODE=yes # 2. make makesum build # 3. upload the *-cache archive # 4. set DEV_UPDATE_MODE=no # 5. make clean makesum DEV_UPDATE_MODE= no DEPS_CACHE_DIR= ${WRKSRC}/plugins/magma .if (${DEV_UPDATE_MODE} == "yes") NPM_ARGS= install post-build: @cd ${DEPS_CACHE_DIR} && ${TAR} czf ${DISTDIR}/${DIST_SUBDIR}/${PORTNAME}-cache-${DISTVERSION}${EXTRACT_SUFX} node_modules @${ECHO} "(!!!) Please upload the cache archive: ${DISTDIR}/${DIST_SUBDIR}/${PORTNAME}-cache-${DISTVERSION}${EXTRACT_SUFX}" .else MASTER_SITES+= LOCAL/acm/caldera/:npm_cache -DISTFILES+= ${PORTNAME}-cache-${DISTVERSION}${EXTRACT_SUFX}:npm_cache +DISTFILES+= ${PORTNAME}-cache-${DISTVERSION}${EXTRACT_SUFX}:npm_cache \ + ${PORTNAME}-freebsd-logos.tar.gz:npm_cache NPM_ARGS= run build post-extract: @${MV} ${WRKDIR}/node_modules ${WRKSRC}/plugins/magma/ + @${MV} ${WRKDIR}/freebsd.red.svg ${WRKSRC}/plugins/magma/src/assets/img/graph/freebsd-icon-privileged.svg + @${MV} ${WRKDIR}/freebsd.black.svg ${WRKSRC}/plugins/magma/src/assets/img/graph/freebsd-icon.svg ${RM} -R ${WRKSRC}/.github cd ${WRKSRC} && ${RM} .coveragerc .dockerignore .eslintrc.js .flake8 \ .git* .pre* .stylelintrc.json Dockerfile .endif post-patch: cd ${WRKSRC} && \ ${FIND} . -type f -name "*.orig" -exec ${RM} "{}" \; ${REINPLACE_CMD} -i "" -e 's|4.2.0|${PORTVERSION}|g' ${WRKSRC}/app/version.py do-build: @cd ${WRKSRC}/plugins/magma && \ ${NPM_BIN} ${NPM_ARGS} post-build: ${RM} -r ${WRKSRC}/plugins/magma/node_modules pre-install: @${RM} -f ${PLIST} @${ECHO_CMD} "@mode 640" > ${PLIST} @${ECHO_CMD} "@owner caldera" >> ${PLIST} @${ECHO_CMD} "@group caldera" >> ${PLIST} @cd ${WRKSRC} && \ ${FIND} * -type f | ${SORT} | ${SED} -e 's|^|%%WWWDIR%%/|' >> ${PLIST} @${ECHO_CMD} "@mode 750" >> ${PLIST} @${ECHO_CMD} "@owner caldera" >> ${PLIST} @${ECHO_CMD} "@group caldera" >> ${PLIST} @cd ${WRKSRC} && \ ${FIND} * -type d | ${SORT} -r | ${SED} -e 's|^|@dir %%WWWDIR%%/|' >> ${PLIST} @${ECHO_CMD} "@mode" >> ${PLIST} @${ECHO_CMD} "@owner" >> ${PLIST} @${ECHO_CMD} "@group" >> ${PLIST} do-install: @cd ${WRKSRC} && ${COPYTREE_SHARE} . ${STAGEDIR}/${WWWDIR} .include diff --git a/security/caldera/files/caldera.in b/security/caldera/files/caldera.in index d08e4fae7d5c..7b57c2e33777 100644 --- a/security/caldera/files/caldera.in +++ b/security/caldera/files/caldera.in @@ -1,85 +1,90 @@ #!/bin/sh # PROVIDE: caldera # REQUIRE: NETWORKING # KEYWORD: shutdown # # Configuration settings for caldera in /etc/rc.conf: # # caldera_enable: run caldera as service (default=NO) # caldera_flags: additional flags for caldera server # . /etc/rc.subr name=caldera rcvar=caldera_enable load_rc_config ${name} export PATH="${PATH}:/usr/local/bin:/usr/local/sbin" : ${caldera_enable:=NO} : ${caldera_flags="--insecure"} caldera_env="GOCACHE=/tmp/caldera/.cache GOMODCACHE=/tmp/caldera/.vendor" caldera_user="caldera" caldera_wwwdir="%%WWWDIR%%" caldera_logfile="/var/log/caldera.log" pidfile="/var/run/${name}.pid" python_command="%%PYTHON_CMD%%" python_script="${caldera_wwwdir}/server.py" start_cmd=${name}_start status_cmd=${name}_status stop_cmd=${name}_stop restart_cmd=${name}_restart extra_commands="status" caldera_start() { if [ ! -f ${pidfile} ] then cd ${caldera_wwwdir} && \ daemon -u ${caldera_user} -p ${pidfile} -t ${name} -o ${caldera_logfile} \ ${python_command} ${python_script} \ ${caldera_flags} echo "Starting ${name}" else echo "${name} is running as pid" `cat ${pidfile}` fi } caldera_status() { # If running, show pid if [ -f ${pidfile} ] then echo "${name} is running as pid" `cat ${pidfile}` else echo "${name} is not running" fi } caldera_stop() { if [ -f ${pidfile} ] then - kill `cat ${pidfile}` + kill -TERM `cat ${pidfile}` rm ${pidfile} + + pid_child=$(pgrep -U ${caldera_user}) + if [ ! -z "${pid_child}" ]; then + kill -TERM ${pid_child} + fi echo "Stopping ${name}" else echo "${name} not running? (check ${pidfile})." fi } caldera_restart() { echo "Performing restart ${name}" caldera_stop sleep 3 caldera_start } run_rc_command "$1" diff --git a/security/caldera/files/patch-plugins_atomic_app_atomic__svc.py b/security/caldera/files/patch-plugins_atomic_app_atomic__svc.py index 04e18806665b..7f5883b80a9f 100644 --- a/security/caldera/files/patch-plugins_atomic_app_atomic__svc.py +++ b/security/caldera/files/patch-plugins_atomic_app_atomic__svc.py @@ -1,20 +1,72 @@ ---- plugins/atomic/app/atomic_svc.py.orig 2022-08-11 15:59:49 UTC +--- plugins/atomic/app/atomic_svc.py.orig 2024-02-13 20:49:23 UTC +++ plugins/atomic/app/atomic_svc.py -@@ -13,7 +13,7 @@ from app.utility.base_world import BaseWorld +@@ -5,6 +5,7 @@ import os + import re + import shutil + import yaml ++import platform + + from collections import defaultdict + from subprocess import DEVNULL, STDOUT, check_call +@@ -13,7 +14,7 @@ from app.utility.base_world import BaseWorld from app.utility.base_service import BaseService from app.objects.c_agent import Agent -PLATFORMS = dict(windows='windows', macos='darwin', linux='linux') +PLATFORMS = dict(windows='windows', macos='darwin', linux='linux', freebsd='freebsd') EXECUTORS = dict(command_prompt='cmd', sh='sh', powershell='psh', bash='sh') RE_VARIABLE = re.compile('(#{(.*?)})', re.DOTALL) PREFIX_HASH_LEN = 6 -@@ -45,7 +45,7 @@ class AtomicService(BaseService): +@@ -45,11 +46,17 @@ class AtomicService(BaseService): the `repo_url` parameter (eg. if you want to use a fork). """ if not repo_url: - repo_url = 'https://github.com/redcanaryco/atomic-red-team.git' -+ repo_url = 'https://github.com/alonsobsd/atomic-red-team.git' ++ if platform.system() == "FreeBSD": ++ repo_url = 'https://github.com/alonsobsd/atomic-red-team.git' ++ else: ++ repo_url = 'https://github.com/redcanaryco/atomic-red-team.git' if not os.path.exists(self.repo_dir) or not os.listdir(self.repo_dir): self.log.debug('cloning repo %s' % repo_url) +- check_call(['git', 'clone', '--depth', '1', repo_url, self.repo_dir], stdout=DEVNULL, stderr=STDOUT) ++ if platform.system() == "FreeBSD": ++ check_call(['git', 'clone', '-b', 'atomic', '--depth', '1', repo_url, self.repo_dir], stdout=DEVNULL, stderr=STDOUT) ++ else: ++ check_call(['git', 'clone', '--depth', '1', repo_url, self.repo_dir], stdout=DEVNULL, stderr=STDOUT) + self.log.debug('clone complete') + + async def populate_data_directory(self, path_yaml=None): +@@ -132,6 +139,11 @@ class AtomicService(BaseService): + return path.replace('\\', '/') + return path + ++ @staticmethod ++ def _platform_system(): ++ _platform = platform.system() ++ return _platform ++ + def _catch_path_to_atomics_folder(self, string_to_analyse, platform): + """ + Catch a path to the atomics/ folder in the `string_to_analyse` variable, +@@ -302,6 +314,10 @@ class AtomicService(BaseService): + Return True if an ability was saved. + """ + ability_id = hashlib.md5(json.dumps(test).encode()).hexdigest() ++ list_name = 'supported_platforms' ++ ++ if self._platform_system() == 'FreeBSD': ++ list_name = 'run_on' + + tactics_li = self.technique_to_tactics.get(entries['attack_technique'], ['redcanary-unknown']) + tactic = 'multiple' if len(tactics_li) > 1 else tactics_li[0] +@@ -317,7 +333,8 @@ class AtomicService(BaseService): + ), + platforms=dict() + ) +- for p in test['supported_platforms']: ++ ++ for p in test[list_name]: + if test['executor']['name'] != 'manual': + # manual tests are expected to be run manually by a human, no automation is provided + executor = EXECUTORS.get(test['executor']['name'], 'unknown')