From 290966cc90055f8123652e59cdf71fd02cd22f8f Mon Sep 17 00:00:00 2001 From: deacon-mp Date: Wed, 20 May 2026 10:46:12 -0400 Subject: [PATCH] atomic_svc: heal a missing enterprise-attack.json when repo is up to date The startup refresh only ran 'git reset --hard' when the ART checkout was behind upstream, so a deleted enterprise-attack.json was NOT restored when the repo was already at the latest commit. The plugin then imported every ability under the 'redcanary-unknown' tactic instead of its real ATT&CK tactic. Restore the STIX (and any other deleted tracked file) via 'git checkout' from the local object store whenever it is found missing after the fetch/update step. This needs no network, so it also heals an offline checkout. Verified: deleting only enterprise-attack.json (repo present, up to date) now restores it on boot and re-imports 1754 abilities across all 15 tactics rather than collapsing them into redcanary-unknown. Co-Authored-By: Claude Opus 4.7 --- app/atomic_svc.py | 33 ++++++++++++++++++++++++--------- 1 file changed, 24 insertions(+), 9 deletions(-) diff --git a/app/atomic_svc.py b/app/atomic_svc.py index 94192a6..8ec8611 100644 --- a/app/atomic_svc.py +++ b/app/atomic_svc.py @@ -77,11 +77,13 @@ async def clone_atomic_red_team_repo(self, repo_url=None): async def _update_atomic_red_team_repo(self): """ - Fast-forward an existing shallow ART checkout to the latest upstream HEAD. + Fast-forward an existing shallow ART checkout to the latest upstream HEAD + and heal the bundled enterprise-attack.json if it went missing. - A `git reset --hard` also restores the bundled enterprise-attack.json if - it went missing. Any failure (no network, git absent, corrupt checkout) - is logged and swallowed so the existing on-disk copy keeps working. + Updating to the latest commit needs network and is best-effort: failures + (offline, git absent, corrupt checkout) are logged and swallowed so the + on-disk copy keeps working. Restoring a deleted tracked file (eg. the + STIX) is done from the local git object store and works offline. """ try: run(['git', 'fetch', '--depth', '1', 'origin', 'HEAD'], @@ -92,15 +94,28 @@ async def _update_atomic_red_team_repo(self): stdout=PIPE, stderr=DEVNULL, text=True).stdout.strip() if local == remote: self.log.debug('Atomic Red Team repo already up to date') - return - self.log.debug('updating Atomic Red Team repo to latest') - run(['git', 'reset', '--hard', 'FETCH_HEAD'], - cwd=self.repo_dir, check=True, stdout=DEVNULL, stderr=DEVNULL) - self.log.debug('Atomic Red Team repo updated') + else: + self.log.debug('updating Atomic Red Team repo to latest') + run(['git', 'reset', '--hard', 'FETCH_HEAD'], + cwd=self.repo_dir, check=True, stdout=DEVNULL, stderr=DEVNULL) + self.log.debug('Atomic Red Team repo updated') except (CalledProcessError, OSError) as e: self.log.warning('Could not update Atomic Red Team repo (offline?): %s. ' 'Using the existing on-disk copy.' % e) + # Heal a missing bundled STIX even when the repo is otherwise up to date + # (eg. the file was deleted but no new upstream commit exists to reset + # to). Restoring a tracked file from the local checkout needs no network. + stix_rel = os.path.join('atomic_red_team', 'enterprise-attack.json') + if not os.path.isfile(os.path.join(self.repo_dir, stix_rel)): + self.log.debug('enterprise-attack.json missing; restoring from local checkout') + try: + run(['git', 'checkout', '--', stix_rel], + cwd=self.repo_dir, check=True, stdout=DEVNULL, stderr=DEVNULL) + except (CalledProcessError, OSError) as e: + self.log.warning('Could not restore enterprise-attack.json from local ' + 'checkout: %s' % e) + async def populate_data_directory(self, path_yaml=None): """ Populate the 'data' directory with the Atomic Red Team abilities.