-
Notifications
You must be signed in to change notification settings - Fork 636
Open
Labels
Description
Describe the bug
When attempting to hard-delete a sketch that has already been soft-deleted, the operation fails because Sketch.get_with_acl(sketch_id) returns a 404. As a result, the sketch cannot be fully removed.
Additionally, while the data in OpenSearch is deleted, the uploaded files in the persistent volume are not removed, leaving orphaned files behind.
To Reproduce
Steps to reproduce the behavior:
- Create a sketch
- Soft-delete the sketch (using the Web UI)
- Attempt to hard-delete the same sketch using the API
- Observe that:
- The API call returns 404.
- Files in the upload persistent volume remain undeleted.
Expected behavior
- Hard-delete should succeed even if the sketch was previously soft-deleted.
- All associated files in the upload persistent volume should be removed.
Additional context
This is the deletion script I've used
#!/usr/bin/env python3
"""Script to perform hard delete of a Timesketch sketch given an ID.
This script uses the Timesketch API client to permanently delete a sketch
and all its associated data (timelines, events, views, etc.).
Requirements:
pip install timesketch-api-client
Usage:
python delete_sketch.py --sketch-id <SKETCH_ID> --server <SERVER_URL> --username <USERNAME>
"""
import argparse
import getpass
import requests
from timesketch_api_client import client
def hard_delete_sketch(server_url, username, password, sketch_id):
"""Perform a hard delete of a sketch.
Args:
server_url (str): URL of the Timesketch server
username (str): Username for authentication
password (str): Password for authentication
sketch_id (int): ID of the sketch to delete
Returns:
bool: True if deletion was successful, False otherwise
"""
try:
# Create API client
print(f"Connecting to Timesketch server: {server_url}")
ts_client = client.TimesketchApi(
host_uri=server_url,
username=username,
password=password
)
# Try to get the sketch (this may fail if already soft-deleted)
print(f"Fetching sketch with ID: {sketch_id}")
try:
sketch = ts_client.get_sketch(sketch_id)
sketch_name = sketch.name
sketch_description = sketch.description
sketch_status = sketch.status
# Check if sketch is archived
if sketch.is_archived():
print("ERROR: Sketch is archived. Please unarchive it before deletion.")
print("You can unarchive using: sketch.unarchive()")
return False
# Display sketch information
print("\n" + "="*60)
print("SKETCH TO BE DELETED:")
print("="*60)
print(f"ID: {sketch.id}")
print(f"Name: {sketch_name}")
print(f"Description: {sketch_description}")
print(f"Status: {sketch_status}")
# List timelines that will be deleted
print("\nTIMELINES TO BE DELETED:")
timelines = sketch.list_timelines()
for timeline in timelines:
print(f" - Timeline ID {timeline.id}: {timeline.name}")
print("="*60)
except RuntimeError as e:
# Sketch might be soft-deleted already, which returns 404
if "404" in str(e):
print("\n" + "="*60)
print("SKETCH APPEARS TO BE SOFT-DELETED")
print("="*60)
print(f"Sketch ID: {sketch_id}")
print("Status: Already soft-deleted (not accessible via normal API)")
print("\nWARNING: Cannot retrieve full sketch details.")
print("The sketch may have already been marked as deleted.")
print("="*60)
sketch_name = f"Sketch {sketch_id}"
else:
raise
# Confirm deletion
confirmation = input("\nThis will PERMANENTLY delete the sketch and all associated data.\n"
"Type 'DELETE' to confirm: ")
if confirmation != "DELETE":
print("Deletion cancelled.")
return False
# Perform hard delete using direct API call
print("\nPerforming hard delete...")
# Make direct DELETE request with force=true parameter
api_url = f"{ts_client.api_root}/sketches/{sketch_id}/?force=true"
response = ts_client.session.delete(api_url)
if response.status_code == 200:
print(f"✓ Sketch {sketch_id} successfully deleted.")
return True
elif response.status_code == 404:
print(f"✗ Sketch {sketch_id} not found. It may have already been deleted.")
return False
elif response.status_code == 403:
print(f"✗ Permission denied. You must be an admin to perform hard deletes.")
print(f"Response: {response.text}")
return False
else:
print(f"✗ Failed to delete sketch. Status code: {response.status_code}")
print(f"Response: {response.text}")
return False
except RuntimeError as e:
print(f"ERROR: Failed to delete sketch {sketch_id}. {e}")
return False
except Exception as e:
print(f"ERROR: Unexpected error occurred: {e}")
import traceback
traceback.print_exc()
return False
def main():
"""Main function to parse arguments and execute deletion."""
parser = argparse.ArgumentParser(
description="Hard delete a Timesketch sketch and all associated data."
)
parser.add_argument(
"--sketch-id",
type=int,
required=True,
help="ID of the sketch to delete"
)
parser.add_argument(
"--server",
type=str,
required=True,
help="Timesketch server URL (e.g., https://timesketch.example.com)"
)
parser.add_argument(
"--username",
type=str,
required=True,
help="Username for authentication"
)
parser.add_argument(
"--password",
type=str,
required=False,
help="Password for authentication (will prompt if not provided)"
)
args = parser.parse_args()
# Get password if not provided
password = args.password
if not password:
password = getpass.getpass(f"Password for {args.username}: ")
# Perform deletion
success = hard_delete_sketch(
server_url=args.server,
username=args.username,
password=password,
sketch_id=args.sketch_id
)
exit(0 if success else 1)
if __name__ == "__main__":
main()