@@ -424,5 +424,61 @@ def serve(
424424 uvicorn .run (app , ** uvicorn_kw )
425425
426426
427+ async def _apply_protection (level : str ):
428+ from .lib .config import ProtectionLevel
429+ from .lib .dal import apply_file_protection
430+
431+ try :
432+ target = ProtectionLevel (level )
433+ except ValueError :
434+ print (f"[red]Invalid protection level: { level } [/red]" )
435+ print (f"Valid options: { ', ' .join (p .value for p in ProtectionLevel )} " )
436+ raise typer .Exit (code = 1 ) from None
437+
438+ settings = get_settings ()
439+ secret = settings .file_secret .get_secret_value () if settings .file_secret else None
440+
441+ if target in (ProtectionLevel .HMAC , ProtectionLevel .ENCRYPT ) and not secret :
442+ print (f"[red]FILE_SECRET is required for protection level '{ level } '[/red]" )
443+ raise typer .Exit (code = 1 )
444+
445+ # Secret is also needed to decrypt existing .enc files
446+ if target != ProtectionLevel .ENCRYPT and not secret :
447+ # Check if any .enc files exist that would need decryption
448+ base = Path (settings .file_store_dir )
449+ if base .exists ():
450+ for _p in base .rglob ("*.enc" ):
451+ print ("[red]FILE_SECRET is required to decrypt existing encrypted files[/red]" )
452+ raise typer .Exit (code = 1 )
453+
454+ print (f"Applying protection level: [bold]{ target .value } [/bold]" )
455+ stats = await apply_file_protection (target , secret )
456+ print (f"Done. processed={ stats ['processed' ]} skipped={ stats ['skipped' ]} errors={ stats ['errors' ]} " )
457+ if stats ["errors" ] > 0 :
458+ raise typer .Exit (code = 1 )
459+
460+
461+ @app .command ("protect" )
462+ def protect (
463+ level : str = typer .Argument (
464+ help = "Protection level to apply: none, hash, hmac, encrypt" ,
465+ ),
466+ ):
467+ """Apply file protection to all artifacts in the file store.
468+
469+ Walks all artifact directories, reads each file (decrypting if needed),
470+ then writes it with the target protection level and cleans up old
471+ protection artifacts.
472+
473+ Examples:
474+ si-cli protect hash # add SHA-512 hash sidecars
475+ si-cli protect hmac # add HMAC-SHA-512 sidecars
476+ si-cli protect encrypt # encrypt files (requires FILE_SECRET)
477+ si-cli protect none # remove all protection, decrypt files
478+ """
479+ validate_settings (dump = False )
480+ asyncio .run (_apply_protection (level ))
481+
482+
427483if __name__ == "__main__" :
428484 app ()
0 commit comments