@@ -193,6 +193,9 @@ def _load_pack_registry(packs_root: Optional[Path]) -> tuple[dict, dict]:
193193 ".secrets-ignore" ,
194194 "Author_image.png" ,
195195 "CHANGELOG.md" ,
196+ # Policy/config files that live inside List directories but are not
197+ # list descriptors or data files — read directly by framework validators.
198+ "shadow_mode_policy.json" ,
196199}
197200
198201# Files in these directories are skipped. They have their own SDK schema
@@ -333,8 +336,12 @@ def content_type_from_path(path: Path) -> Optional[str]:
333336 if content_dir == "Playbooks" and path .suffix .lower () in (".yml" , ".yaml" ):
334337 return "playbook"
335338
336- # Accept both .json and .txt in Lists/ — XSIAM exports can produce either
339+ # Accept both .json and .txt in Lists/ — XSIAM exports can produce either.
340+ # Never process _data.json files — they are the data half of the two-file
341+ # list structure already in the repo, not a new contribution to normalise.
337342 if content_dir == "Lists" and path .suffix .lower () in (".json" , ".txt" ):
343+ if path .stem .endswith ("_data" ):
344+ return None
338345 return "list"
339346
340347 if content_dir == "Scripts" :
@@ -1197,6 +1204,59 @@ def process_file(
11971204 return True , False
11981205
11991206 canon = list_canonical_name (path , override_name )
1207+
1208+ # Determine the target directory — where the canonical files live.
1209+ if out_dir is not None :
1210+ target_dir = Path (out_dir ) / canon
1211+ else :
1212+ lists_parent = path .parent
1213+ if lists_parent .name == canon :
1214+ target_dir = lists_parent
1215+ else :
1216+ p = path .parent
1217+ while p != p .parent and p .name != "Lists" :
1218+ p = p .parent
1219+ target_dir = p / canon
1220+
1221+ data_path = target_dir / f"{ canon } _data.json"
1222+ desc_path = target_dir / f"{ canon } .json"
1223+
1224+ # ── UPDATE MODE: _data.json already exists ────────────────────────────
1225+ # The contributor is updating an existing list. Write the new content
1226+ # directly to _data.json and leave the descriptor completely untouched.
1227+ # No descriptor modification, no fromVersion injection, no split.
1228+ if data_path .exists ():
1229+ # Check if content actually changed
1230+ try :
1231+ existing = json .loads (data_path .read_text (encoding = "utf-8" ))
1232+ except Exception :
1233+ existing = {}
1234+
1235+ if existing == data :
1236+ print (OK (" ✓ already clean" ))
1237+ return True , False
1238+
1239+ changes = ["data file updated (existing list)" ]
1240+ prefix = "(dry-run) " if dry_run else ""
1241+ for c in changes :
1242+ print (f" { prefix } { OK ('●' )} { c } " )
1243+
1244+ if not dry_run :
1245+ target_dir .mkdir (parents = True , exist_ok = True )
1246+ data_path .write_text (
1247+ json .dumps (data , indent = 2 , ensure_ascii = False ) + "\n " ,
1248+ encoding = "utf-8" ,
1249+ )
1250+ print (f" { OK ('→' )} { data_path } (data)" )
1251+ if path .resolve () != data_path .resolve ():
1252+ path .unlink ()
1253+ print (f" { OK ('✗' )} removed { path .name } (replaced by canonical data file)" )
1254+
1255+ return True , True
1256+
1257+ # ── CREATE MODE: new list, no existing _data.json ─────────────────────
1258+ # The contributor is adding a brand new list. Create both the descriptor
1259+ # and data files from the contribution.
12001260 descriptor , changes , data_out = normalize_list (data , canon )
12011261
12021262 if not changes :
@@ -1208,47 +1268,22 @@ def process_file(
12081268 print (f" { prefix } { OK ('●' )} { c } " )
12091269
12101270 if not dry_run :
1211- # Determine the target directory.
1212- # Repo structure: Lists/<ListName>/<ListName>.json + <ListName>_data.json
1213- # If the file is already inside a correctly-named subdirectory, use it.
1214- # If the file is at the Lists/ level or has a different name, create
1215- # the subdirectory and write both files there.
1216- if out_dir is not None :
1217- target_dir = Path (out_dir ) / canon
1218- else :
1219- # Check if we are already inside Lists/<canon>/
1220- lists_parent = path .parent
1221- if lists_parent .name == canon :
1222- # Already in the right subdirectory
1223- target_dir = lists_parent
1224- else :
1225- # At Lists/ level or wrong directory — create canonical subdir
1226- # Walk up to find the Lists/ directory
1227- p = path .parent
1228- while p != p .parent and p .name != "Lists" :
1229- p = p .parent
1230- target_dir = p / canon
1231-
12321271 target_dir .mkdir (parents = True , exist_ok = True )
12331272
1234- # Write the descriptor file: <ListName>.json
1235- desc_path = target_dir / f" { canon } .json"
1236- desc_path .write_text (
1237- json .dumps (descriptor , indent = 2 , ensure_ascii = False ) + "\n " ,
1238- encoding = "utf-8" ,
1239- )
1240- print (f" { OK ('→' )} { desc_path } (descriptor)" )
1273+ # Write descriptor only if it doesn't already exist
1274+ if not desc_path . exists ():
1275+ desc_path .write_text (
1276+ json .dumps (descriptor , indent = 2 , ensure_ascii = False ) + "\n " ,
1277+ encoding = "utf-8" ,
1278+ )
1279+ print (f" { OK ('→' )} { desc_path } (descriptor)" )
12411280
1242- # Write the data file: <ListName>_data.json
1243- data_path = target_dir / f"{ canon } _data.json"
12441281 data_path .write_text (
12451282 json .dumps (data_out , indent = 2 , ensure_ascii = False ) + "\n " ,
12461283 encoding = "utf-8" ,
12471284 )
12481285 print (f" { OK ('→' )} { data_path } (data)" )
12491286
1250- # Remove the original file if it is in a different location or has a
1251- # different name — prevents stale files with duplicate identity
12521287 if path .resolve () != desc_path .resolve () and path .resolve () != data_path .resolve ():
12531288 path .unlink ()
12541289 print (f" { OK ('✗' )} removed { path .name } (replaced by canonical files)" )
0 commit comments