feat(stix): STIX→MISP download export (per-attacker + fleet)
Adds GET /api/v1/attackers/{uuid}/export/misp and
GET /api/v1/attackers/export/misp backed by misp_export.py, which
converts existing STIX bundles to MISP events via misp-stix
ExternalSTIX2toMISPParser. Fleet endpoint emits {response:[...]}
collection (one event per attacker). Frontend: STIX/MISP buttons on
AttackerDetail header and Attackers list. 13 new tests green.
This commit is contained in:
@@ -14,19 +14,32 @@ interface Props {
|
||||
export const AttackerHeader: React.FC<Props> = ({ attacker }) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const handleStixDownload = async () => {
|
||||
const _download = async (endpoint: string, filename: string) => {
|
||||
try {
|
||||
const res = await api.get(`/attackers/${attacker.uuid}/export/stix`, { responseType: 'blob' });
|
||||
const res = await api.get(endpoint, { responseType: 'blob' });
|
||||
const href = URL.createObjectURL(res.data);
|
||||
const a = document.createElement('a');
|
||||
a.href = href;
|
||||
a.download = `decnet-attacker-${attacker.uuid.slice(0, 8)}.stix.json`;
|
||||
a.download = filename;
|
||||
a.click();
|
||||
URL.revokeObjectURL(href);
|
||||
} catch {
|
||||
// best-effort
|
||||
}
|
||||
};
|
||||
|
||||
const handleStixDownload = () =>
|
||||
_download(
|
||||
`/attackers/${attacker.uuid}/export/stix`,
|
||||
`decnet-attacker-${attacker.uuid.slice(0, 8)}.stix.json`,
|
||||
);
|
||||
|
||||
const handleMispDownload = () =>
|
||||
_download(
|
||||
`/attackers/${attacker.uuid}/export/misp`,
|
||||
`decnet-attacker-${attacker.uuid.slice(0, 8)}.misp.json`,
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '16px' }}>
|
||||
<Crosshair size={32} className="violet-accent" />
|
||||
@@ -60,16 +73,26 @@ export const AttackerHeader: React.FC<Props> = ({ attacker }) => {
|
||||
IDENTITY · {attacker.identity_id.slice(0, 8)}
|
||||
</span>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
style={{ marginLeft: 'auto' }}
|
||||
title="Download STIX 2.1 bundle for this attacker"
|
||||
onClick={handleStixDownload}
|
||||
>
|
||||
<Download size={12} />
|
||||
<span style={{ marginLeft: 6 }}>STIX</span>
|
||||
</button>
|
||||
<div style={{ marginLeft: 'auto', display: 'flex', gap: 8 }}>
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
title="Download STIX 2.1 bundle for this attacker"
|
||||
onClick={handleStixDownload}
|
||||
>
|
||||
<Download size={12} />
|
||||
<span style={{ marginLeft: 6 }}>STIX</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn"
|
||||
title="Download MISP event for this attacker"
|
||||
onClick={handleMispDownload}
|
||||
>
|
||||
<Download size={12} />
|
||||
<span style={{ marginLeft: 6 }}>MISP</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -117,12 +117,12 @@ const Attackers: React.FC = () => {
|
||||
|
||||
const totalPages = Math.max(1, Math.ceil(total / limit));
|
||||
|
||||
const handleExport = async () => {
|
||||
const _fleetDownload = async (endpoint: string, fallback: string) => {
|
||||
try {
|
||||
const res = await api.get('/attackers/export/stix', { responseType: 'blob' });
|
||||
const res = await api.get(endpoint, { responseType: 'blob' });
|
||||
const disposition: string = res.headers['content-disposition'] || '';
|
||||
const match = disposition.match(/filename="([^"]+)"/);
|
||||
const filename = match ? match[1] : 'decnet-fleet.stix.json';
|
||||
const filename = match ? match[1] : fallback;
|
||||
const url = URL.createObjectURL(new Blob([res.data], { type: 'application/json' }));
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
@@ -134,6 +134,9 @@ const Attackers: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleExport = () => _fleetDownload('/attackers/export/stix', 'decnet-fleet.stix.json');
|
||||
const handleMispExport = () => _fleetDownload('/attackers/export/misp', 'decnet-fleet.misp.json');
|
||||
|
||||
const activityCounts = attackers.reduce(
|
||||
(acc, a) => { acc[deriveActivity(a)]++; return acc; },
|
||||
{ active: 0, passive: 0, inactive: 0 } as Record<ActivityTier, number>,
|
||||
@@ -236,7 +239,17 @@ const Attackers: React.FC = () => {
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 6 }}
|
||||
>
|
||||
<Download size={13} />
|
||||
EXPORT
|
||||
STIX
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn-ghost"
|
||||
onClick={handleMispExport}
|
||||
title="Export all attackers as MISP collection"
|
||||
style={{ display: 'flex', alignItems: 'center', gap: 6 }}
|
||||
>
|
||||
<Download size={13} />
|
||||
MISP
|
||||
</button>
|
||||
<div className="pager">
|
||||
<span className="dim">Page {page} of {totalPages}</span>
|
||||
|
||||
Reference in New Issue
Block a user