# How LazyOwn Attack Surface Visualization Works with Bloodhound Data Integration

> Discover how LazyOwn visualizes your attack surface using Bloodhound data. Explore critical attack vectors with interactive AD environment maps.

- Repository: [Grisuno/lazyown](https://github.com/grisuno/lazyown)
- Tags: how-to-guide
- Published: 2026-03-02

---

**LazyOwn transforms raw Bloodhound Active Directory exports into interactive attack surface visualizations by parsing ZIP archives server-side, extracting nodes and privilege relationships, and rendering them through a Vis.js frontend with curated lists of critical attack vectors.**

The **LazyOwn** C2 framework (available at `grisuno/lazyown`) provides Red Team operators with a streamlined approach to analyzing Active Directory environments through integrated **attack surface visualization with Bloodhound data integration**. This capability bridges the gap between raw Bloodhound collection data and actionable security insights, converting complex AD privilege relationships into an explorable graph interface.

## Bloodhound Data Processing Pipeline

The visualization workflow follows a strict three-stage pipeline implemented in [`lazyc2.py`](https://github.com/grisuno/lazyown/blob/main/lazyc2.py). First, the **`/upload_zip`** endpoint (lines 3985-4040) accepts Bloodhound export archives with validation and security checks. Second, the **`process_bloodhound_zip`** function (lines 508-610) parses the JSON collections and constructs graph primitives. Third, **`extract_attack_vectors`** analyzes these structures to identify privileged accounts and dangerous permission edges before the frontend renders the interactive network.

## Secure File Upload and Validation

The upload handler implements security-hardened file processing to prevent directory traversal and ensure data integrity. Located in [`lazyc2.py`](https://github.com/grisuno/lazyown/blob/main/lazyc2.py), the `upload_zip_file` function generates **UUID-based temporary filenames** to prevent collisions and canonicalizes all paths using `os.path.abspath` to verify files remain within the configured `UPLOAD_FOLDER`.

```python
@app.route('/upload_zip', methods=['POST'])
def upload_zip_file():
    """Handles the file upload, processes the BloodHound ZIP, and prepares data for visualization."""
    if 'file' not in request.files:
        return render_template('index.html', error="No file part")
    file = request.files['file']
    if file.filename == '' or not file.filename.lower().endswith('.zip'):
        return render_template('index.html', error="Only ZIP files are allowed")

    os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
    unique_filename = f"{uuid.uuid4().hex}.zip"
    filepath = os.path.join(app.config['UPLOAD_FOLDER'], unique_filename)

    # Security check – ensure the resolved path stays inside the upload folder

    if not os.path.abspath(filepath).startswith(os.path.abspath(app.config['UPLOAD_FOLDER'])):
        return render_template('index.html', error="Invalid file path")

    file.save(filepath)
    nodes, edges, error_message, ad_data = process_bloodhound_zip(filepath)
    
    try: os.remove(filepath)
    except Exception: pass

    if error_message:
        return render_template('index.html', error=error_message)

    return render_template('surface.html', nodes=nodes, edges=edges, ad_data=ad_data)

```

The function strictly validates file extensions, enforces path containment, and guarantees cleanup of temporary ZIP files regardless of parsing success or failure.

## Extracting Nodes and Edges from Bloodhound Exports

### Parsing ZIP Contents

The **`process_bloodhound_zip`** function in [`lazyc2.py`](https://github.com/grisuno/lazyown/blob/main/lazyc2.py) (lines 508-610) orchestrates the extraction of Active Directory objects from Bloodhound's multi-file JSON format. It iterates through every `*.json` file within the archive—typically including [`users.json`](https://github.com/grisuno/lazyown/blob/main/users.json), [`groups.json`](https://github.com/grisuno/lazyown/blob/main/groups.json), [`computers.json`](https://github.com/grisuno/lazyown/blob/main/computers.json), and relationship files—building a comprehensive graph representation.

```python
def process_bloodhound_zip(zip_filepath):
    """
    Processes a BloodHound ZIP file to extract nodes and edges for graph visualization.
    Returns: (nodes, edges, error_message, ad_data)
    """
    nodes_data = {}
    edges_data = []
    error_message = None

    try:
        with zipfile.ZipFile(zip_filepath, 'r') as zip_ref:
            json_files = [n for n in zip_ref.namelist() if n.endswith('.json')]
            if not json_files:
                return [], [], "No JSON files found in the ZIP", {}

            for name in json_files:
                with zip_ref.open(name) as f:
                    data = json.load(f)
                    items = data.get('data', []) if isinstance(data, dict) else data
                    for item in items:
                        # Node extraction

                        if 'ObjectIdentifier' in item and 'Properties' in item:
                            node_id = item['ObjectIdentifier']
                            if node_id not in nodes_data:
                                props = item['Properties']
                                nodes_data[node_id] = {
                                    'id': node_id,
                                    'label': props.get('name', node_id),
                                    'title': json.dumps(props, indent=2),
                                    'type': name.split('_')[1].replace('.json', '')
                                }

                        # Edge extraction from ACEs and Group Memberships

                        if 'Aces' in item and isinstance(item['Aces'], list):
                            for ace in item['Aces']:
                                if 'PrincipalSID' in ace and 'RightName' in ace:
                                    edges_data.append({
                                        'from': item['ObjectIdentifier'],
                                        'to': ace['PrincipalSID'],
                                        'label': ace['RightName']
                                    })
                        if 'PrimaryGroupSID' in item and item['PrimaryGroupSID']:
                            edges_data.append({
                                'from': item['ObjectIdentifier'],
                                'to': item['PrimaryGroupSID'],
                                'label': 'MemberOf'
                            })
    except FileNotFoundError:
        error_message = f"File not found: {zip_filepath}"
    except zipfile.BadZipFile:
        error_message = f"Invalid or corrupted ZIP file: {zip_filepath}"
    except Exception:
        error_message = "An unexpected error occurred while processing the ZIP"

    ad_data = extract_attack_vectors(list(nodes_data.values()), edges_data)
    return list(nodes_data.values()), edges_data, error_message, ad_data

```

### Building the Graph Structure

For each Bloodhound object, the parser constructs **nodes** using the `ObjectIdentifier` as a unique ID and populates properties including human-readable labels, JSON-formatted tooltips (`title`), and object types derived from filenames. **Edges** are constructed from two primary relationship sources:

- **ACE relationships**: Access Control Entries defining permissions like `GenericAll`, `WriteDacl`, and `WriteOwner`
- **Group memberships**: `PrimaryGroupSID` relationships indicating `MemberOf` associations

After building the raw graph, the function invokes `extract_attack_vectors` to derive high-value security insights.

## Identifying Critical Attack Vectors

The **`extract_attack_vectors`** function transforms raw graph data into actionable intelligence by identifying **privileged accounts** and **dangerous permissions** according to the LazyOwn source code. It searches for high-value AD groups (Domain Admins, Enterprise Admins, Administrators) and flags edges containing high-risk rights defined in the `dangerous_rights` list.

```python
def extract_attack_vectors(nodes, edges):
    """
    Analyses BloodHound nodes and edges to extract critical attack vectors for AD compromise.
    Returns a dict with privileged_accounts, dangerous_permissions, potential_attack_paths, misconfigurations.
    """
    ad_data = {
        'privileged_accounts': [],
        'dangerous_permissions': [],
        'potential_attack_paths': [],
        'misconfigurations': []
    }

    # Privileged accounts detection

    for node in nodes:
        if node.get('type') in ['User', 'Group'] and node.get('label', '').lower() in [
            'domain admins', 'enterprise admins', 'administrators'
        ]:
            ad_data['privileged_accounts'].append({
                'id': node['id'],
                'label': node['label'],
                'type': node['type'],
                'details': node['title']
            })

    # Dangerous permissions detection

    dangerous_rights = ['GenericAll', 'WriteDacl', 'WriteOwner',
                       'Owns', 'AllExtendedRights', 'DCSync']
    for edge in edges:
        if edge['label'] in dangerous_rights:
            src = next((n for n in nodes if n['id'] == edge['from']), None)
            dst = next((n for n in nodes if n['id'] == edge['to']), None)
            if src and dst:
                ad_data['dangerous_permissions'].append({
                    'from': src['label'],
                    'to': dst['label'],
                    'right': edge['label'],
                    'source_type': src['type'],
                    'target_type': dst['type']
                })

    return ad_data

```

The function specifically flags **GenericAll**, **WriteDacl**, **WriteOwner**, **Owns**, **AllExtendedRights**, and **DCSync** permissions as critical attack vectors, resolving source and target node labels to provide human-readable context for security operators.

## Interactive Frontend Visualization

The **Vis.js** network library powers the client-side rendering in [`templates/surface.html`](https://github.com/grisuno/lazyown/blob/main/templates/surface.html) and [`templates/graph.html`](https://github.com/grisuno/lazyown/blob/main/templates/graph.html). The Flask backend injects JSON-encoded node and edge datasets directly into the Jinja2 templates using the `tojson` filter, eliminating the need for additional API calls.

```jinja
{% set nodes_json = nodes|tojson %}
{% set edges_json = edges|tojson %}
{% set ad_data_json = ad_data|tojson %}

```

The JavaScript initialization creates interactive DataSets that support real-time filtering, physics-based layouts, and clustering:

```javascript
const nodes = new vis.DataSet({{ nodes_json }});
const edges = new vis.DataSet({{ edges_json }});

const container = document.getElementById('mynetwork');
const data = { nodes, edges };
const options = {/* filters, clustering, physics, etc. */};

const network = new vis.Network(container, data, options);

```

The **[`surface.html`](https://github.com/grisuno/lazyown/blob/main/surface.html)** template renders an attack vector accordion displaying the four categories from `ad_data`: privileged accounts, dangerous permissions, potential attack paths, and misconfigurations. UI controls allow operators to filter by node type, edge relationship, and depth, while statistical panels update dynamically as the graph changes.

## Practical Implementation Examples

### Uploading Bloodhound Data via cURL

Operators can programmatically upload Bloodhound exports using standard HTTP POST requests:

```bash
curl -X POST http://localhost:5000/upload_zip \
     -F "file=@/path/to/BloodHound-Data.zip"

```

### Programmatic Parsing in Python

The parsing functions can be imported directly for custom analysis workflows:

```python
from lazyc2 import process_bloodhound_zip

zip_path = "samples/BloodHound-Data.zip"
nodes, edges, err, ad_data = process_bloodhound_zip(zip_path)

if err:
    print(f"Error: {err}")
else:
    print(f"Found {len(nodes)} nodes and {len(edges)} edges")
    print("Privileged accounts:")
    for acct in ad_data['privileged_accounts']:
        print(f" * {acct['label']} ({acct['type']})")

```

### Rendering in Custom Flask Views

The visualization components integrate seamlessly into custom route handlers:

```python
@app.route('/demo')
def demo():
    nodes, edges, _, ad_data = process_bloodhound_zip('sample.zip')
    return render_template('surface.html',
                           nodes=nodes,
                           edges=edges,
                           ad_data=ad_data)

```

## Summary

LazyOwn's **attack surface visualization with Bloodhound data integration** provides a comprehensive pipeline for Active Directory security analysis:

- **Secure upload handling** in [`lazyc2.py`](https://github.com/grisuno/lazyown/blob/main/lazyc2.py) validates ZIP archives and prevents path traversal through UUID-based filenames and path canonicalization
- **Bloodhound JSON parsing** extracts nodes from `ObjectIdentifier` fields and builds relationship edges from ACE and group membership data
- **Attack vector extraction** identifies privileged accounts and dangerous permissions including GenericAll, WriteDacl, and DCSync rights
- **Vis.js frontend rendering** in [`templates/surface.html`](https://github.com/grisuno/lazyown/blob/main/templates/surface.html) delivers interactive graph exploration with real-time filtering and curated security insights
- **Jinja2 data injection** eliminates frontend API dependencies by embedding parsed graph data directly into the rendered HTML

## Frequently Asked Questions

### How does LazyOwn prevent directory traversal during Bloodhound ZIP uploads?

The `upload_zip_file` function in [`lazyc2.py`](https://github.com/grisuno/lazyown/blob/main/lazyc2.py) resolves absolute paths using `os.path.abspath` and validates that the target filepath starts with the absolute path of the configured `UPLOAD_FOLDER`. This canonicalization check ensures uploaded files cannot escape the designated storage directory regardless of malicious filename inputs.

### What specific Bloodhound relationship types does LazyOwn extract for visualization?

According to the source code in [`lazyc2.py`](https://github.com/grisuno/lazyown/blob/main/lazyc2.py), the parser extracts two primary relationship categories: **ACE edges** from the `Aces` array (representing permissions like GenericAll, WriteDacl, and WriteOwner) and **group membership edges** from `PrimaryGroupSID` fields (rendered as MemberOf relationships). These cover the critical privilege escalation pathways in Active Directory environments.

### Can the attack vector detection logic be extended to identify custom misconfigurations?

Yes. The `extract_attack_vectors` function structure in [`lazyc2.py`](https://github.com/grisuno/lazyown/blob/main/lazyc2.py) includes placeholder arrays for `potential_attack_paths` and `misconfigurations` specifically reserved for custom heuristics. Security teams can extend this function to implement GPO analysis, Kerberoasting detection, or domain-specific privilege anomalies while maintaining the existing visualization pipeline through [`surface.html`](https://github.com/grisuno/lazyown/blob/main/surface.html).

### What frontend technologies render the interactive attack surface graph?

The visualization layer combines **Jinja2 templating** (for server-side data injection), **Vis.js Network** (for the interactive graph canvas), and **Bootstrap** components (for the sidebar controls and attack vector accordion). These are defined in [`templates/surface.html`](https://github.com/grisuno/lazyown/blob/main/templates/surface.html) and [`templates/graph.html`](https://github.com/grisuno/lazyown/blob/main/templates/graph.html), with the Vis.js DataSets receiving pre-processed node and edge arrays directly from the Flask backend.