Security Analytics - v1.1.0
AI features
v1.1.0 adds three AI-assisted surfaces to the package, built on top of artisanpack-ui/ai. Each is opt-in (togglable at runtime via the shared feature registry), advisory-only (nothing runs automatically), and short-circuits cleanly when the toggle is off or when credentials cannot be resolved — no LLM calls happen in either case.
The three features
| Feature key | Agent class | Livewire component | Default model | Purpose |
|---|---|---|---|---|
security.threat_triage |
ThreatTriageAgent |
ThreatTriagePanel |
claude-sonnet-4-6 |
Plain-language severity + recommended actions for a single SecurityEvent. |
security.anomaly_summary |
AnomalySummaryAgent |
AnomalySummaryPanel |
claude-haiku-4-5-20251001 |
Periodic digest of unusual events over a configurable window (1–720 hours). |
security.incident_response |
IncidentResponseAgent |
IncidentResponsePanel |
claude-opus-4-7 |
Advisory-only next-step suggestions for an open incident. Never triggers actions. |
All three implement Laravel\Ai\Contracts\Agent and Laravel\Ai\Contracts\HasStructuredOutput, so the response is validated JSON matching each agent's schema — no free-text parsing on your side.
Discovery
The service provider exposes an aiFeatures() method that artisanpack-ui/ai's boot pass walks looking for feature registrations:
public function aiFeatures(): array
{
return [
'security.threat_triage' => [ 'agent' => ThreatTriageAgent::class, ... ],
'security.anomaly_summary' => [ 'agent' => AnomalySummaryAgent::class, ... ],
'security.incident_response' => [ 'agent' => IncidentResponseAgent::class, ... ],
];
}
No manual registration on the host app's side. If artisanpack-ui/ai isn't installed the discovery pass never runs and the features stay dormant.
Livewire panels
Each agent has a matching Livewire trigger panel registered under a namespaced tag. Drop them into any Blade view:
{{-- On the security-event detail surface --}}
<livewire:security-analytics.threat-triage-panel :event-id="$event->id" />
{{-- Somewhere on the dashboard --}}
<livewire:security-analytics.anomaly-summary-panel />
{{-- On the incident detail surface --}}
<livewire:security-analytics.incident-response-panel :incident-id="$incident->id" />
Each panel:
- Gates on the standard
view-security-events/view-security-dashboardabilities - Marks target IDs
#[Locked]so the client can't rewire the target via the wire protocol - Renders a disabled state when the feature is toggled off or credentials aren't configured (no LLM call fires)
- Logs any provider exception server-side and shows the user a generic error message (so Guzzle stack traces don't leak API keys into the DOM)
Shipped Blade views
Plain HTML + Tailwind by design — the package doesn't depend on artisanpack-ui/livewire-ui-components. Override the shipped views by shadowing them:
resources/views/vendor/security-analytics/livewire/threat-triage-panel.blade.php
resources/views/vendor/security-analytics/livewire/anomaly-summary-panel.blade.php
resources/views/vendor/security-analytics/livewire/incident-response-panel.blade.php
Laravel resolves your overrides before the package defaults.
Direct invocation
The three agents are also usable from PHP (jobs, commands, controllers) without the Livewire panel:
use ArtisanPackUI\SecurityAnalytics\AI\Agents\ThreatTriageAgent;
$triage = ThreatTriageAgent::for( $securityEvent )->run();
// [
// 'severity' => 'high',
// 'summary' => '…',
// 'recommended_actions' => [ [ 'step' => '…', 'urgency' => 'immediate' ], … ],
// 'related_events' => [ 42, 43, 44 ],
// ]
Input shapes accepted:
| Agent | Input |
|---|---|
ThreatTriageAgent |
SecurityEvent model, event id (int), or [ 'event' => [...], 'related' => [...], 'context' => [...] ] |
AnomalySummaryAgent |
Window in hours (int), or [ 'window_hours' => int, 'anomalies' => [...], 'statistics' => [...] ] |
IncidentResponseAgent |
SecurityIncident model, incident id (int), or [ 'incident' => [...], 'timeline' => [...], 'playbooks' => [...] ] |
Toggling features
Each feature is togglable at runtime via artisanpack-ui/ai's feature registry:
use ArtisanPackUI\Ai\Contracts\FeatureRegistry;
$registry = app( FeatureRegistry::class );
$registry->disable( 'security.threat_triage' );
$registry->enable( 'security.threat_triage' );
$registry->isToggleOn( 'security.threat_triage' );
$registry->isEnabled( 'security.threat_triage' ); // toggle + credentials
When disabled, ThreatTriageAgent::for(...)->run() throws FeatureDisabledException. The Livewire panels catch this and render a disabled state; direct callers can catch it themselves.
Overriding prompts and models
Prompt and model overrides layer via artisanpack.ai.features.{feature_key}:
// config/artisanpack/ai.php
return [
'features' => [
'security.threat_triage' => [
'model' => 'claude-haiku-4-5-20251001',
'instructions' => <<<'PROMPT'
Your team's custom triage prompt goes here.
Keep the schema fields (severity/summary/…) intact.
PROMPT,
],
],
];
Precedence (top wins):
FeatureSettingsstore (usually written by the AI Settings admin UI)artisanpack.ai.features.{key}.instructions/.modelconfig value- Class defaults on the agent
Instructions are applied per-run through the CallsLaravelAi trait, so overrides don't require a redeploy.
Credentials
Credentials resolve via artisanpack-ui/ai's CredentialResolver chain:
- Runtime override on the agent (
->withCredentials($creds)) FeatureSettingsstore (per-feature)- Env vars (
ARTISANPACK_AI_API_KEY,ARTISANPACK_AI_PROVIDER,ARTISANPACK_AI_DEFAULT_MODEL, …)
The security-analytics agents don't hard-code Anthropic — flip ARTISANPACK_AI_PROVIDER to openai / gemini / ollama / etc. and the same agents route to that provider. Default models on each agent are Anthropic-flavored but overridable via config as above.
Under Octane, RoadRunner, and queue workers, the CallsLaravelAi::configureProvider() step invalidates laravel/ai's cached provider so a rotated key (or a per-tenant credential injection between agents) actually lands — see the Fixed block in CHANGELOG.md under 1.1.0.
Caching
Each agent caches responses using a content-derived fingerprint:
ThreatTriageAgent— event fields + related-event window + contextAnomalySummaryAgent— window + per-anomaly (id, severity, detector) tuplesIncidentResponseAgent— incident id + severity + status + updated_at + timeline contents
TTL and the cache store are the AI package's artisanpack.ai.cache.* settings — see artisanpack-ui/ai's docs. Set $agent->cacheable = false (or per-feature config) to disable caching entirely.
Cost + performance
- ThreatTriageAgent (Sonnet 4.6) — one round-trip per event; typical response ~1–3 seconds. Related-event fetch is capped at 10 rows.
- AnomalySummaryAgent (Haiku 4.5) — one round-trip per window; anomaly rows capped at 50 for the LLM payload, but aggregate
total_count/by_severity/by_detectorare computed via DBGROUP BYso they're accurate for busy windows. - IncidentResponseAgent (Opus 4.7) — one round-trip per invocation. Deeper model tier, so ~3–8 seconds per call; use sparingly on high-volume queue paths.
The AgentUsageRecorded event fires after every run — subscribe to it to bill tokens back to a per-tenant budget (artisanpack-ui/ai also ships a BudgetSettings service that consumes the same event).
Advisory-only
None of the three agents trigger response actions. They emit structured advice; the responder decides what to run. The action-execution surface remains Analytics\IncidentResponse\IncidentResponder (10 pluggable actions, playbook-driven approval). See Incident response for that surface.
Follow-ups
- Incident response — the manual + playbook-driven action surface the AI advises on
- Dashboard — where the AI panels typically get mounted
artisanpack-ui/aidocs — the AI Settings admin UI, credential resolver chain, feature registry, budget tracking