|
1 | 1 | // ==UserScript== |
2 | 2 | // @name SentinelOne: PowerQuery Custom Menu |
3 | | -// @version 3 |
| 3 | +// @version 4 |
4 | 4 | // @description Custom menu for threat hunting rules with a compact UI, cell copy on query page, and quick unpin feature. |
5 | 5 | // @author https://github.com/LasCC |
6 | 6 | // @match *://*.sentinelone.net/query* |
7 | 7 | // @match *://*.sentinelone.net/events* |
8 | | -// @downloadURL https://raw.githubusercontent.com/LasCC/SentinelOne-Userscript/master/userscript.js |
9 | | -// @updateURL https://raw.githubusercontent.com/LasCC/SentinelOne-Userscript/master/userscript.js |
| 8 | +// @downloadURL https://raw.githubusercontent.com/LasCC/SentinelOne-Userscript/master/sentinelone_query.user.js |
| 9 | +// @updateURL https://raw.githubusercontent.com/LasCC/SentinelOne-Userscript/master/sentinelone_query.user.js |
10 | 10 | // @grant GM_xmlhttpRequest |
11 | 11 | // @grant GM_setValue |
12 | 12 | // @grant GM_getValue |
|
37 | 37 | const copyIconSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`; |
38 | 38 | const checkIconSVG = `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"></polyline></svg>`; |
39 | 39 |
|
| 40 | + // Stable per-category accent colors (loosely MITRE-tactic aligned). Unknown |
| 41 | + // categories fall back to a deterministic hashed hue so they stay distinct. |
| 42 | + const CATEGORY_COLORS = { |
| 43 | + "Credential Access": "#e5484d", |
| 44 | + "Defense Evasion": "#f76b15", |
| 45 | + "Defense Evasion & Execution": "#f76b15", |
| 46 | + "Discovery & Reconnaissance": "#3e63dd", |
| 47 | + "Lateral Movement": "#e93d82", |
| 48 | + "Command & Control": "#d6409f", |
| 49 | + "Exfiltration": "#ab4aba", |
| 50 | + "Execution & TTPs": "#8e4ec6", |
| 51 | + "Execution & LOLBAS": "#5b5bd6", |
| 52 | + "Execution & Persistence": "#7c66dc", |
| 53 | + "Installation & Persistence": "#12a594", |
| 54 | + "Privilege Escalation": "#e54666", |
| 55 | + "Impact": "#dc3b5d", |
| 56 | + "Malware & Threats": "#ca3214", |
| 57 | + "Forensics & Investigation": "#3a9e6e", |
| 58 | + "Helper & Utilities": "#6e6e77", |
| 59 | + macOS: "#0091ff", |
| 60 | + }; |
| 61 | + |
| 62 | + function categoryColor(category) { |
| 63 | + if (!category) return "var(--s1-N-40-color)"; |
| 64 | + if (CATEGORY_COLORS[category]) return CATEGORY_COLORS[category]; |
| 65 | + const lc = category.toLowerCase(); |
| 66 | + if (lc.includes("evasion")) return "#f76b15"; |
| 67 | + if (lc.includes("credential")) return "#e5484d"; |
| 68 | + if (lc.includes("persistence")) return "#12a594"; |
| 69 | + if (lc.includes("execution")) return "#8e4ec6"; |
| 70 | + let h = 0; |
| 71 | + for (let i = 0; i < category.length; i++) { |
| 72 | + h = (h * 31 + category.charCodeAt(i)) >>> 0; |
| 73 | + } |
| 74 | + return `hsl(${h % 360} 60% 55%)`; |
| 75 | + } |
| 76 | + |
40 | 77 | function getPinnedQueries() { |
41 | 78 | if (pinnedCache) return pinnedCache; |
42 | 79 | try { |
|
263 | 300 | const searchInput = document.createElement("input"); |
264 | 301 | searchInput.type = "text"; |
265 | 302 | searchInput.className = "hunting-queries-search"; |
266 | | - searchInput.placeholder = "Search queries..."; |
267 | | - searchInput.setAttribute("aria-label", "Search hunting queries"); |
| 303 | + searchInput.placeholder = "Search name, category or query..."; |
| 304 | + searchInput.setAttribute("aria-label", "Search hunting queries by name, category or query content"); |
268 | 305 |
|
269 | 306 | const clearButton = document.createElement("button"); |
270 | 307 | clearButton.className = "hunting-queries-clear"; |
|
339 | 376 |
|
340 | 377 | if (category === "Pinned") { |
341 | 378 | tabButton.innerHTML = `<span class="hunting-queries-tab-content">${starIconSVG} <span>Pinned</span></span>`; |
342 | | - } else { |
| 379 | + } else if (category === "All") { |
343 | 380 | tabButton.innerHTML = `<span class="hunting-queries-tab-content">${category}</span>`; |
| 381 | + } else { |
| 382 | + // Category tabs get a matching color dot. |
| 383 | + tabButton.style.setProperty("--cat-color", categoryColor(category)); |
| 384 | + tabButton.innerHTML = `<span class="hunting-queries-tab-content"><span class="hunting-queries-tab-dot"></span>${category}</span>`; |
344 | 385 | } |
345 | 386 |
|
346 | 387 | let count = 0; |
|
421 | 462 | queryObj.category === activeCategory; |
422 | 463 | const matchesSearch = |
423 | 464 | !lowerSearchTerm || |
424 | | - queryObj.name.toLowerCase().includes(lowerSearchTerm); |
| 465 | + queryObj.name.toLowerCase().includes(lowerSearchTerm) || |
| 466 | + (queryObj.category && |
| 467 | + queryObj.category.toLowerCase().includes(lowerSearchTerm)) || |
| 468 | + (queryObj.query && |
| 469 | + queryObj.query.toLowerCase().includes(lowerSearchTerm)); |
425 | 470 | return matchesCategory && matchesSearch; |
426 | 471 | }); |
427 | 472 |
|
|
455 | 500 | ) { |
456 | 501 | const categoryHeader = document.createElement("div"); |
457 | 502 | categoryHeader.className = "hunting-queries-category-header"; |
| 503 | + categoryHeader.style.setProperty( |
| 504 | + "--cat-color", |
| 505 | + categoryColor(category) |
| 506 | + ); |
458 | 507 | categoryHeader.textContent = category; |
459 | 508 | navigationDiv.appendChild(categoryHeader); |
460 | 509 | } |
461 | 510 |
|
462 | 511 | queries.forEach((queryObj) => { |
| 512 | + const isPinned = pinnedQueryNames.includes(queryObj.name); |
463 | 513 | const queryItem = document.createElement("div"); |
464 | 514 | queryItem.className = "hunting-queries-item"; |
| 515 | + if (isPinned) queryItem.classList.add("is-pinned"); |
| 516 | + // Per-category accent (left border + tag dot) drives the CSS via a custom property. |
| 517 | + queryItem.style.setProperty( |
| 518 | + "--cat-color", |
| 519 | + categoryColor(queryObj.category) |
| 520 | + ); |
| 521 | + // Native tooltip previews the PowerQuery so it can be verified before running. |
| 522 | + if (queryObj.query) { |
| 523 | + queryItem.title = |
| 524 | + queryObj.query.length > 600 |
| 525 | + ? queryObj.query.slice(0, 600) + "…" |
| 526 | + : queryObj.query; |
| 527 | + } |
465 | 528 |
|
466 | 529 | const queryContent = document.createElement("div"); |
467 | 530 | queryContent.className = "hunting-queries-item-content"; |
|
474 | 537 | if (queryObj.category && activeCategory === "All") { |
475 | 538 | const categoryTag = document.createElement("span"); |
476 | 539 | categoryTag.className = "hunting-queries-item-category"; |
477 | | - categoryTag.textContent = queryObj.category; |
| 540 | + const dot = document.createElement("span"); |
| 541 | + dot.className = "hunting-queries-item-category-dot"; |
| 542 | + categoryTag.appendChild(dot); |
| 543 | + categoryTag.appendChild( |
| 544 | + document.createTextNode(queryObj.category) |
| 545 | + ); |
478 | 546 | queryMeta.appendChild(categoryTag); |
479 | 547 | } |
480 | 548 | queryContent.appendChild(queryName); |
|
487 | 555 | const pinButton = document.createElement("button"); |
488 | 556 | pinButton.className = "hunting-queries-pin-btn"; |
489 | 557 | pinButton.innerHTML = starIconSVG; |
490 | | - if (pinnedQueryNames.includes(queryObj.name)) { |
| 558 | + if (isPinned) { |
491 | 559 | pinButton.classList.add("pinned"); |
492 | 560 | pinButton.title = "Unpin query"; |
493 | 561 | } else { |
|
500 | 568 | togglePinQuery(queryObj.name); |
501 | 569 | }); |
502 | 570 |
|
| 571 | + const copyButton = document.createElement("button"); |
| 572 | + copyButton.className = "hunting-queries-copy-btn"; |
| 573 | + copyButton.innerHTML = copyIconSVG; |
| 574 | + copyButton.title = "Copy query to clipboard"; |
| 575 | + copyButton.setAttribute("aria-label", "Copy query to clipboard"); |
| 576 | + copyButton.addEventListener("click", (e) => { |
| 577 | + e.preventDefault(); |
| 578 | + e.stopPropagation(); |
| 579 | + navigator.clipboard |
| 580 | + .writeText(queryObj.query) |
| 581 | + .then(() => { |
| 582 | + copyButton.innerHTML = checkIconSVG; |
| 583 | + copyButton.classList.add("copied"); |
| 584 | + showNotification("Query copied to clipboard!"); |
| 585 | + setTimeout(() => { |
| 586 | + copyButton.innerHTML = copyIconSVG; |
| 587 | + copyButton.classList.remove("copied"); |
| 588 | + }, 2000); |
| 589 | + }) |
| 590 | + .catch((err) => { |
| 591 | + console.error("Failed to copy query:", err); |
| 592 | + }); |
| 593 | + }); |
| 594 | + |
503 | 595 | const useButton = document.createElement("button"); |
504 | 596 | useButton.className = "hunting-queries-use-btn"; |
505 | | - useButton.innerHTML = "Use"; |
| 597 | + useButton.innerHTML = "Run"; |
506 | 598 | useButton.title = "Insert and run this query"; |
507 | 599 |
|
508 | 600 | queryActions.appendChild(pinButton); |
| 601 | + queryActions.appendChild(copyButton); |
509 | 602 | queryActions.appendChild(useButton); |
510 | 603 | queryItem.appendChild(queryContent); |
511 | 604 | queryItem.appendChild(queryActions); |
|
848 | 941 | .hunting-queries-tab:hover { border-color: var(--s1-P-50-color); color: var(--s1-P-50-color); } |
849 | 942 | .hunting-queries-tab.active { background: var(--s1-P-50-color); color: var(--s1-const-N-0-color, #fff); border-color: var(--s1-P-50-color); } |
850 | 943 | .hunting-queries-tab-content { display: flex; align-items: center; gap: 4px; } |
| 944 | + .hunting-queries-tab-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--cat-color, var(--s1-N-40-color)); flex-shrink: 0; } |
| 945 | + .hunting-queries-tab.active .hunting-queries-tab-dot { background: var(--s1-const-N-0-color, #fff); } |
851 | 946 | .hunting-queries-tab-badge { |
852 | 947 | background: var(--s1-N-20-color); color: var(--s1-N-70-color); font-size: 9px; font-weight: 600; |
853 | 948 | padding: 1px 5px; border-radius: 8px; margin-left: 4px; |
|
859 | 954 | .hunting-queries-category-header { |
860 | 955 | padding: 6px var(--s1-distance-5) 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; |
861 | 956 | background: var(--s1-N-10-color); color: var(--s1-N-70-color); border-bottom: 1px solid var(--s1-N-20-color); margin-bottom: 4px; |
| 957 | + border-left: 3px solid var(--cat-color, transparent); |
862 | 958 | } |
863 | | - .hunting-queries-item { display: flex; align-items: center; justify-content: space-between; padding: 6px var(--s1-distance-5); cursor: pointer; transition: background-color 0.2s ease, border-left-color 0.2s ease; border-left: 3px solid transparent; } |
| 959 | + .hunting-queries-item { display: flex; align-items: center; justify-content: space-between; padding: 6px var(--s1-distance-5); cursor: pointer; transition: background-color 0.2s ease, border-left-color 0.2s ease; border-left: 3px solid var(--cat-color, transparent); } |
864 | 960 | .hunting-queries-item:hover { background: var(--s1-N-10-color); border-left-color: var(--s1-P-50-color); } |
865 | 961 | .hunting-queries-item:focus, .hunting-queries-item:focus-within { outline: none; background: var(--s1-N-15-color); border-left-color: var(--s1-P-50-color); } |
866 | 962 | .hunting-queries-item-content { flex: 1; min-width: 0; } |
867 | 963 | .hunting-queries-item-name { font-size: 13px; font-weight: 500; color: var(--s1-N-100-color); line-height: 1.3; } |
868 | 964 | .hunting-queries-item-meta { display: flex; flex-direction: column; gap: 4px; margin-top: 2px; } |
869 | | - .hunting-queries-item-category { display: inline-block; font-size: 10px; background: var(--s1-N-15-color); color: var(--s1-N-70-color); padding: 1px 5px; border-radius: var(--s1-border-radius-3); font-weight: 500; width: fit-content; } |
870 | | - .hunting-queries-item-actions { display: flex; align-items: center; opacity: 0; transition: opacity 0.2s ease; gap: 6px; } |
871 | | - .hunting-queries-item:hover .hunting-queries-item-actions, .hunting-queries-item:focus-within .hunting-queries-item-actions { opacity: 1; } |
872 | | - .hunting-queries-pin-btn { background: none; border: none; cursor: pointer; padding: 4px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--s1-N-40-color); transition: all 0.2s ease; } |
| 965 | + .hunting-queries-item-category { display: inline-flex; align-items: center; gap: 5px; font-size: 10px; background: var(--s1-N-15-color); color: var(--s1-N-70-color); padding: 1px 6px 1px 5px; border-radius: var(--s1-border-radius-3); font-weight: 500; width: fit-content; } |
| 966 | + .hunting-queries-item-category-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--cat-color, var(--s1-N-40-color)); flex-shrink: 0; } |
| 967 | + .hunting-queries-item-actions { display: flex; align-items: center; gap: 6px; } |
| 968 | + /* Pin + copy + run fade in on hover/focus; a pinned star stays lit at rest. */ |
| 969 | + .hunting-queries-pin-btn, .hunting-queries-copy-btn, .hunting-queries-use-btn { opacity: 0; transition: opacity 0.2s ease, background-color 0.2s ease, color 0.2s ease; } |
| 970 | + .hunting-queries-item:hover .hunting-queries-pin-btn, |
| 971 | + .hunting-queries-item:hover .hunting-queries-copy-btn, |
| 972 | + .hunting-queries-item:hover .hunting-queries-use-btn, |
| 973 | + .hunting-queries-item:focus-within .hunting-queries-pin-btn, |
| 974 | + .hunting-queries-item:focus-within .hunting-queries-copy-btn, |
| 975 | + .hunting-queries-item:focus-within .hunting-queries-use-btn { opacity: 1; } |
| 976 | + .hunting-queries-pin-btn.pinned { opacity: 1; } |
| 977 | + .hunting-queries-pin-btn { background: none; border: none; cursor: pointer; padding: 4px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--s1-N-40-color); } |
873 | 978 | .hunting-queries-pin-btn:hover { background: var(--s1-N-15-color); color: var(--s1-P-50-color); } |
874 | 979 | .hunting-queries-pin-btn .star-icon { fill: none; stroke: currentColor; } |
875 | 980 | .hunting-queries-pin-btn.pinned .star-icon { color: var(--s1-P-50-color); fill: var(--s1-P-50-color); stroke: var(--s1-P-50-color); } |
876 | 981 | .hunting-queries-pin-btn.pinned:hover { color: var(--s1-P-40-color); } |
877 | | - .hunting-queries-use-btn { background: var(--s1-P-50-color); color: var(--s1-const-N-0-color, #fff); border: none; padding: 3px 8px; border-radius: var(--s1-border-radius-3); font-size: 11px; font-weight: 500; cursor: pointer; transition: all 0.2s ease; } |
| 982 | + .hunting-queries-copy-btn { background: none; border: none; cursor: pointer; padding: 4px; border-radius: 50%; display: flex; align-items: center; justify-content: center; color: var(--s1-N-40-color); } |
| 983 | + .hunting-queries-copy-btn:hover { background: var(--s1-N-15-color); color: var(--s1-P-50-color); } |
| 984 | + .hunting-queries-copy-btn svg { width: 14px; height: 14px; } |
| 985 | + .hunting-queries-copy-btn.copied { color: var(--s1-G-50-color); } |
| 986 | + .hunting-queries-use-btn { background: var(--s1-P-50-color); color: var(--s1-const-N-0-color, #fff); border: none; padding: 3px 8px; border-radius: var(--s1-border-radius-3); font-size: 11px; font-weight: 500; cursor: pointer; } |
878 | 987 | .hunting-queries-use-btn:hover { background: var(--s1-P-40-color); } |
879 | 988 | .hunting-queries-highlight { background: var(--s1-N-20-color); color: var(--s1-N-100-color); padding: 0 2px; border-radius: 2px; font-weight: 500; } |
880 | 989 |
|
|
0 commit comments