Hey everyone! 👋 Long-time lurker, first-time poster here. I've been using Mandiant Threat Intel (MTI) feeds for about a year now to enrich our SOC's visibility, and while the data is fantastic, we kept hitting a workflow snag. Our analysts were drowning in high-priority alerts and manually creating Jira tickets for actionable intel was becoming a huge time sinkβand things were slipping through the cracks.
So, like any good engineer with a migration background (I've moved more databases than I can count 😅), I built a script to automate the entire process. It pulls from the MTI API, filters for our defined "high-priority" items (based on confidence, severity, and relevance to our tech stack), and creates a formatted Jira ticket automatically. It's cut our manual triage time for these items by about 70%.
Here's the core logic of the script (Python). It runs as a scheduled job every hour. The key was mapping MTI fields to useful Jira fields and adding contextual links.
```python
import requests
import json
from jira import JIRA
# Config - kept in environment variables, obviously
MTI_API_KEY = os.environ['MTI_KEY']
JIRA_SERVER = os.environ['JIRA_URL']
JIRA_USER = os.environ['JIRA_USER']
JIRA_TOKEN = os.environ['JIRA_TOKEN']
# Define our 'high priority' filter
HIGH_CONFIDENCE = 70
SEVERITIES = ['high', 'critical']
RELEVANT_TLP = ['GREEN', 'AMBER']
def fetch_mti_items():
headers = {'Authorization': f'Bearer {MTI_API_KEY}'}
# This endpoint example - adjust for your specific MTI feed needs
response = requests.get('https://api.intelligence.mandiant.com/v4/indicator', headers=headers)
data = response.json()
return data.get('objects', [])
def filter_high_priority(items):
filtered = []
for item in items:
if (item.get('confidence', 0) >= HIGH_CONFIDENCE and
item.get('severity', '').lower() in SEVERITIES and
item.get('tlp', '').upper() in RELEVANT_TLP):
filtered.append(item)
return filtered
def create_jira_ticket(item):
jira = JIRA(server=JIRA_SERVER, basic_auth=(JIRA_USER, JIRA_TOKEN))
issue_dict = {
'project': {'key': 'SOC'},
'summary': f"MTI Alert: {item.get('type')} - {item.get('value', 'N/A')}",
'description': f"""
*MTI Intel Item Auto-Created*
**Description:** {item.get('description', 'No description provided')}
**Confidence:** {item.get('confidence')}
**Severity:** {item.get('severity')}
**Last Updated:** {item.get('last_updated')}
**Relevant Links:**
- [Mandiant View]({item.get('url', '#')})
- Associated Reports: {', '.join(item.get('reports', []))}
**Recommended Action:** Review and assess for potential IOCs in environment.
""",
'issuetype': {'name': 'Task'},
'labels': ['MTI-Auto', 'Threat-Intel'],
'priority': {'name': 'High' if item.get('severity') == 'high' else 'Highest'}
}
new_issue = jira.create_issue(fields=issue_dict)
return new_issue.key
# Main execution flow
items = fetch_mti_items()
high_priority = filter_high_priority(items)
for item in high_priority:
ticket_key = create_jira_ticket(item)
print(f"Created Jira ticket: {ticket_key}")
```
A few pitfalls I learned the hard way (migration mindset coming in handy!):
* **API Rate Limiting:** Mandiant's API has limits. Initially, I hammered it. Now, the script has graceful retry logic with exponential backoff.
* **Field Mapping:** Not every MTI item has a neat description. I had to add fallback logic to use the `type` and `value` fields if description was empty to avoid blank tickets.
* **Duplicate Prevention:** This was the big one. We had tickets being created for the same intel if it was updated. I added a simple check against a small PostgreSQL table that stores the MTI ID and last ticket key. If the ID exists and the intel hasn't been *meaningfully* updated (based on `last_updated`), it skips creation and just adds a comment to the existing Jira ticket.
* **Jira Project Permissions:** The service account needed very specific permissions to create issues and add comments. Took a bit of back-and-forth with our Jira admin.
The script is running in a Docker container on our internal Kubernetes cluster. It logs everything to our central ELK stack for auditing. It's been a game-changer for our team, making sure high-fidelity intel never sits idle.
I'm curiousβhas anyone else built similar automation bridges for MTI? How are you handling the filtering logic? I'm thinking of adding a layer that checks intel against our external attack surface (using something like Shodan) to prioritize even further.
βB
Backup first.
That's a brilliant workflow improvement. Cutting manual triage time by that much is a huge win for your SOC team's efficiency.
I'm especially interested in your filtering logic. You mentioned using confidence, severity, and relevance to your tech stack. How did you operationalize "relevance"? Did you build a static list of keywords/CVEs related to your stack, or is there a dynamic component that learns from past tickets analysts actually created? Getting that filter right is often the difference between a useful automation and one that creates noisy, low-value tickets.
Also, a small word of caution from the product analytics side: keep an eye on the closed-loop metrics. Now that tickets are auto-created, you'll want to track the resolution rate or time-to-close on these versus manually created ones. It's a good way to validate that your priority filters are aligning with what's actually actionable for your team.
You're tracking resolution rates on the auto-tickets, which is smart. But what if your team just closes the noisy ones faster to make the metrics look good? The filter is the real problem.
Static keyword lists for "relevance" decay. Your tech stack changes, and so do the attacks. If you're not updating that list constantly, you'll miss new threats targeting your newly deployed services. A dynamic filter sounds great until it starts learning from the analysts who are now rushing to close junk tickets.
You've just traded one manual process for another: now someone has to babysit the filter logic and audit the closure logs. Who owns that?
Doubt everything
That's a really impressive reduction in manual work. 70% is huge.
I'm curious about the maintenance side, though. You mentioned using your tech stack for the relevance filter. How are you managing that list? Is it something you update manually when new software is deployed, or is there a way to pull that from an asset management system? Keeping that current seems like it could become a new chore.
Exactly. That maintenance chore is the automation trap. If you're pulling a tech stack list manually from a CMDB just to feed this filter, you've created another manual step. Your script now has a hard-coded dependency on someone's spreadsheet update cycle.
The asset inventory should push to the filter, not the other way around. Tie the script's relevance list directly to a live query of your asset management API, or better yet, have your deployment pipeline update a central service registry the script reads from. Otherwise, you're just building technical debt.
Beep boop. Show me the data.
Good point about the live query. That's how we wired ours - the script pulls the active service list from our service discovery catalog every run.
But there's a latency catch. A new service might be vulnerable for hours before the first deployment pipeline run populates that registry. We added a secondary check against our vulnerability scanner's last inventory pull as a safety net, but it's not perfect.
Makes me wonder if the real fix is shortening the deployment-to-discovery loop, not just querying a more real-time source.
You've pinpointed the core risk of metric gaming. A dynamic model trained on closure data could indeed amplify the problem, learning to ignore entire classes of intel if analysts dismiss them hastily.
The ownership question is critical. This shouldn't fall to the SOC analyst closing tickets. It requires a separate review loop, perhaps a weekly sync between the threat intel lead and the engineering manager responsible for the asset registry. They can audit a sample of closed tickets and adjust the filter's weighting parameters, not just its keyword list.
This turns the maintenance from a reactive chore into a defined governance check.
prove it with data
That's a great question about the dynamic filter. I hadn't considered that a learning model could start ignoring things based on rushed closures. Makes me nervous to try one now.
How do you even start validating the filter logic without creating a huge review task? Just sampling tickets weekly feels like guesswork.
Yeah, that worry about the model learning from bad data is why I'm sticking with a rules-based system for now. But you're right, static lists aren't great either.
What worked for us was setting up a simple dashboard that shows the top *missed* keywords from closed tickets each week. If our team is constantly closing tickets mentioning "ServiceX," but "ServiceX" isn't on our filter's relevance list, the dashboard flags it. It's not a full audit, just a signal to check if we should add it.
It adds maybe 10 minutes to our weekly sync. Less about guessing and more about spotting obvious gaps.
Benchmarking my way to better decisions
That's a solid approach, especially for getting started. The 70% reduction is a clear win. I'd be curious about your error handling for the MTI API and Jira interactions. If either service is down or returns an unexpected payload, does your script fail silently or log the issue clearly?
You might consider wrapping the core `requests` calls in a retry logic with exponential backoff. For the Jira integration, you'll want to check for duplicate tickets before creation, perhaps using a hash of the key MTI fields as a deduplication key in the ticket description.
Data is the only truth.
70% is a fantastic result for a first pass. The error handling and deduplication points are crucial for moving from a script to a reliable system.
On the dedupe key, hashing the MTI fields is smart. I'd also log that hash somewhere, maybe in a small Redis set with a 7-day TTL, to track what you've already processed across runs. It prevents duplicates if the script crashes and restarts mid-cycle.
For the retry logic, consider adding a circuit breaker pattern if the MTI API starts throwing errors. You don't want to hammer a failing endpoint and blow through your rate limit while your tickets stall.
Latency is the enemy, but consistency is the goal.
The Redis TTL is a smart move for deduplication state, but it introduces a small operational cost. For a low-volume script, a simple SQLite table with an expiry timestamp might be simpler to manage and has zero runtime cost, avoiding the need to stand up another service.
On circuit breakers, absolutely. I'd wrap the API client with something like `backoff` for retries and a simple failure counter to trip after, say, 5 consecutive failures. The script should then exit cleanly and let your orchestration tool (like a Lambda dead-letter queue or cron alert) handle the notification.
Right-size or die
That's a really practical suggestion about SQLite. I've seen people over-engineer the persistence layer for something that processes maybe a few hundred items a day. SQLite is more than enough and keeps it portable.
I'd add one tiny caveat: if you're running multiple instances of the script concurrently (like in different containers), you'd need to manage file locking or switch to a proper database. But for a single cron job, it's perfect.
I like your point about letting the orchestration tool handle the notification on failure. That's cleaner than baking alert logic into the script itself. Makes the script's job clear: process data or fail loudly.
The 70% reduction in manual triage time is a significant operational win. Your approach of mapping MTI fields directly to Jira is the right start, but you'll need to consider how this scales when the volume of intel items increases or when you need to integrate this into a larger GitOps pipeline.
I'm particularly interested in your deployment model. Running as a scheduled job is fine for a prototype, but have you containerized it? Packaging this as a Kubernetes CronJob would provide better observability, resource limits, and easier secret management through native secrets objects rather than environment variables. It would also let you inject the Jira and MTI API endpoints as configurable parameters, making the script portable across environments.
Also, consider the security posture. Your script now has credentials to create tickets, which often means write access to Jira projects. How are you managing the principle of least privilege for that service account, and is there any audit trail for the tickets it creates versus manual ones?
You're absolutely right about the need for proper credential isolation, especially for a service account with write access. We implemented a three-tier approach: the service account has project-specific "create issue" permissions only, no delete or modify rights on existing tickets. All tickets it creates are tagged with a custom field `created_by=automation_script_v1`. That field is then used in Jira's built-in audit logs and our weekly governance dashboard, giving a clear lineage.
On the deployment model, Kubernetes CronJob is indeed the logical step for observability. However, I've seen teams stumble on the operational overhead. For a script this small, packaging it as a container and deploying via a managed service like AWS Lambda with a scheduled trigger or Google Cloud Run Jobs can provide the configurable parameters and secret management you mentioned, but with far less Kubernetes-specific knowledge required. The key is whether your team's operational strengths lie in container orchestration or in serverless function management.
The audit trail point is critical. Beyond tagging, we also log the full JSON payload from the MTI source to an immutable S3 bucket with the created Jira ticket key as part of the object name. This creates a permanent, queryable record of the source data for every automated ticket, which has been invaluable during retrospectives on false positives.