Skip to content

Hard-delete fails for soft-deleted sketches and leaves orphaned files #3586

@JakePeralta7

Description

@JakePeralta7

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:

  1. Create a sketch
  2. Soft-delete the sketch (using the Web UI)
  3. Attempt to hard-delete the same sketch using the API
  4. 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.

Screenshots
Image

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()

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions