Skip to content

Commit ec96465

Browse files
committed
UI color-coding/search/copy + rename for click-to-install
1 parent f99572f commit ec96465

3 files changed

Lines changed: 128 additions & 19 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,5 @@ jobs:
2424
name: ${{ steps.version.outputs.tag }}
2525
body: ${{ github.event.head_commit.message }}
2626
files: |
27-
userscript.js
27+
sentinelone_query.user.js
2828
s1_powerquery_hunting.json

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,10 @@ A userscript that enhances the SentinelOne PowerQuery interface with a custom th
1919
## Installation
2020

2121
1. Install a userscript manager like [Tampermonkey](https://www.tampermonkey.net/) or [Greasemonkey](https://www.greasespot.net/)
22-
2. Click [here](https://raw.githubusercontent.com/LasCC/SentinelOne-Userscript/refs/heads/master/userscript.js) to install the script
22+
2. Click [here](https://raw.githubusercontent.com/LasCC/SentinelOne-Userscript/refs/heads/master/sentinelone_query.user.js) to install the script
2323
3. The script will automatically load when you visit SentinelOne PowerQuery pages
2424

25-
After the first installation there will be a Tampermonkey popup asking to allow the fetch to grab all the detection rules. You need to click the button "Always allow domain".
25+
The script declares `@connect raw.githubusercontent.com`, so fetching the detection rules is pre-authorized and works without prompting. If your userscript manager still shows a popup asking to allow the fetch, click "Always allow domain".
2626

2727
![Demo Overview](demo/popup_setup.png)
2828

userscript.js renamed to sentinelone_query.user.js

Lines changed: 125 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
// ==UserScript==
22
// @name SentinelOne: PowerQuery Custom Menu
3-
// @version 3
3+
// @version 4
44
// @description Custom menu for threat hunting rules with a compact UI, cell copy on query page, and quick unpin feature.
55
// @author https://github.com/LasCC
66
// @match *://*.sentinelone.net/query*
77
// @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
1010
// @grant GM_xmlhttpRequest
1111
// @grant GM_setValue
1212
// @grant GM_getValue
@@ -37,6 +37,43 @@
3737
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>`;
3838
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>`;
3939

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+
4077
function getPinnedQueries() {
4178
if (pinnedCache) return pinnedCache;
4279
try {
@@ -263,8 +300,8 @@
263300
const searchInput = document.createElement("input");
264301
searchInput.type = "text";
265302
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");
268305

269306
const clearButton = document.createElement("button");
270307
clearButton.className = "hunting-queries-clear";
@@ -339,8 +376,12 @@
339376

340377
if (category === "Pinned") {
341378
tabButton.innerHTML = `<span class="hunting-queries-tab-content">${starIconSVG} <span>Pinned</span></span>`;
342-
} else {
379+
} else if (category === "All") {
343380
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>`;
344385
}
345386

346387
let count = 0;
@@ -421,7 +462,11 @@
421462
queryObj.category === activeCategory;
422463
const matchesSearch =
423464
!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));
425470
return matchesCategory && matchesSearch;
426471
});
427472

@@ -455,13 +500,31 @@
455500
) {
456501
const categoryHeader = document.createElement("div");
457502
categoryHeader.className = "hunting-queries-category-header";
503+
categoryHeader.style.setProperty(
504+
"--cat-color",
505+
categoryColor(category)
506+
);
458507
categoryHeader.textContent = category;
459508
navigationDiv.appendChild(categoryHeader);
460509
}
461510

462511
queries.forEach((queryObj) => {
512+
const isPinned = pinnedQueryNames.includes(queryObj.name);
463513
const queryItem = document.createElement("div");
464514
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+
}
465528

466529
const queryContent = document.createElement("div");
467530
queryContent.className = "hunting-queries-item-content";
@@ -474,7 +537,12 @@
474537
if (queryObj.category && activeCategory === "All") {
475538
const categoryTag = document.createElement("span");
476539
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+
);
478546
queryMeta.appendChild(categoryTag);
479547
}
480548
queryContent.appendChild(queryName);
@@ -487,7 +555,7 @@
487555
const pinButton = document.createElement("button");
488556
pinButton.className = "hunting-queries-pin-btn";
489557
pinButton.innerHTML = starIconSVG;
490-
if (pinnedQueryNames.includes(queryObj.name)) {
558+
if (isPinned) {
491559
pinButton.classList.add("pinned");
492560
pinButton.title = "Unpin query";
493561
} else {
@@ -500,12 +568,37 @@
500568
togglePinQuery(queryObj.name);
501569
});
502570

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+
503595
const useButton = document.createElement("button");
504596
useButton.className = "hunting-queries-use-btn";
505-
useButton.innerHTML = "Use";
597+
useButton.innerHTML = "Run";
506598
useButton.title = "Insert and run this query";
507599

508600
queryActions.appendChild(pinButton);
601+
queryActions.appendChild(copyButton);
509602
queryActions.appendChild(useButton);
510603
queryItem.appendChild(queryContent);
511604
queryItem.appendChild(queryActions);
@@ -848,6 +941,8 @@
848941
.hunting-queries-tab:hover { border-color: var(--s1-P-50-color); color: var(--s1-P-50-color); }
849942
.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); }
850943
.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); }
851946
.hunting-queries-tab-badge {
852947
background: var(--s1-N-20-color); color: var(--s1-N-70-color); font-size: 9px; font-weight: 600;
853948
padding: 1px 5px; border-radius: 8px; margin-left: 4px;
@@ -859,22 +954,36 @@
859954
.hunting-queries-category-header {
860955
padding: 6px var(--s1-distance-5) 4px; font-size: 11px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px;
861956
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);
862958
}
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); }
864960
.hunting-queries-item:hover { background: var(--s1-N-10-color); border-left-color: var(--s1-P-50-color); }
865961
.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); }
866962
.hunting-queries-item-content { flex: 1; min-width: 0; }
867963
.hunting-queries-item-name { font-size: 13px; font-weight: 500; color: var(--s1-N-100-color); line-height: 1.3; }
868964
.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); }
873978
.hunting-queries-pin-btn:hover { background: var(--s1-N-15-color); color: var(--s1-P-50-color); }
874979
.hunting-queries-pin-btn .star-icon { fill: none; stroke: currentColor; }
875980
.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); }
876981
.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; }
878987
.hunting-queries-use-btn:hover { background: var(--s1-P-40-color); }
879988
.hunting-queries-highlight { background: var(--s1-N-20-color); color: var(--s1-N-100-color); padding: 0 2px; border-radius: 2px; font-weight: 500; }
880989

0 commit comments

Comments
 (0)