Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

## [3.0.1] - 2025-07-11

- Added agent language as filter to avoid displaying incompatible modules
- Added folder display to agent modules

## [3.0.0] - 2025-03-25

### Added
Expand Down Expand Up @@ -411,7 +416,9 @@ Including but not limited to:

- Initial Release

[Unreleased]: https://github.com/BC-SECURITY/Starkiller-Sponsors/compare/v3.0.0...HEAD
[Unreleased]: https://github.com/BC-SECURITY/Starkiller-Sponsors/compare/v3.0.1...HEAD

[3.0.1]: https://github.com/BC-SECURITY/Starkiller-Sponsors/compare/v3.0.0...v3.0.1

[3.0.0]: https://github.com/BC-SECURITY/Starkiller-Sponsors/compare/v2.8.2...v3.0.0

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "starkiller",
"version": "3.0.0",
"version": "3.0.1",
"private": true,
"scripts": {
"dev": "vite",
Expand Down
226 changes: 187 additions & 39 deletions src/components/agents/AgentExecuteModule.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,71 @@
<div v-else style="padding: 0 10px 10px 10px">
<info-viewer class="info-viewer" :info="moduleInfo" />
<span class="mr-2 mb-4">
Executing on Agents: {{ agents.join(", ") }}
Executing on Agents:
{{ agents.map((agent) => agent.name || agent.session_id).join(", ") }}
</span>
<technique-chips :techniques="selectedItem.techniques" />
<v-autocomplete
v-model="selectedModule"
:items="selectOptions"
:loading="!reset"
placeholder="Please enter a module name"
outlined
dense
clearable
@change="handleSelect"
/>

<v-alert
v-if="reset && modules.length > 0 && autocompleteOptions.length === 0"
type="error"
>
No modules are compatible with all selected agents.
</v-alert>

<v-row>
<v-col cols="11">
<v-autocomplete
v-model="selectedModule"
:items="autocompleteOptions"
item-text="text"
item-value="value"
:loading="!reset"
placeholder="Search module..."
outlined
dense
clearable
@change="handleSelect"
/>
</v-col>

<v-col cols="1" class="d-flex align-center" style="margin-top: -25px">
<tooltip-button
:icon="showTreeDialog ? 'mdi-folder-open' : 'mdi-folder'"
color="grey lighten-2"
tooltip="Browse modules by folder"
@click="toggleTree"
/>
</v-col>
</v-row>

<v-dialog v-model="showTreeDialog" max-width="500px">
<v-card>
<v-card-title>
<span class="headline">Browse Modules</span>
</v-card-title>
<v-card-text>
<v-treeview
:items="treeOptions"
open-on-click
activatable
:active.sync="treeSelected"
:open.sync="treeOpen"
item-key="id"
item-text="name"
item-children="children"
@update:active="handleTreeSelect"
/>
</v-card-text>
<v-card-actions>
<v-spacer />
<v-btn text color="primary" @click="showTreeDialog = false"
>Close</v-btn
>
</v-card-actions>
</v-card>
</v-dialog>

<v-alert v-if="selectedItem.opsec_safe === false" type="warning">
<v-row align="center">
<v-col
Expand All @@ -32,6 +84,7 @@
</v-col>
</v-row>
</v-alert>

<div
v-if="Object.keys(selectedItem).length > 0"
style="display: flex; flex-direction: row"
Expand All @@ -49,20 +102,23 @@
color="primary"
/>
</div>

<general-form
v-if="reset"
ref="generalform"
v-model="form"
:options="moduleOptions"
/>

<v-btn
v-if="showSubmit"
v-if="selectedModule"
:loading="loading"
color="primary"
@click="create"
>
Submit
</v-btn>

<v-dialog ref="nameDialog" v-model="showDialog" max-width="900px">
<v-card>
<v-card-title>
Expand Down Expand Up @@ -158,37 +214,116 @@ export default {
errorState: false,
ignoreAdminCheck: false,
ignoreLanguageCheck: false,
treeSelected: [],
treeOpen: [],
showTreeDialog: false,
};
},
computed: {
moduleStore() {
return useModuleStore();
},
modules() {
return this.moduleStore.modules.filter((el) => el.enabled === true);
return this.moduleStore.modules.filter((el) => el.enabled);
},
compatibleModules() {
if (!this.agents.length) return this.modules;
const getCompatibleLanguages = (agentLang) => {
switch ((agentLang || "").toLowerCase()) {
case "powershell":
case "csharp":
case "go":
return ["powershell", "csharp", "bof"];
case "ironpython":
return ["powershell", "csharp", "bof", "python"];
case "python":
return ["python"];
default:
return [];
}
};
const agentLangSets = this.agents.map((agent) =>
getCompatibleLanguages(agent.language || agent.type),
);
const sharedLangs = agentLangSets.reduce((acc, curr) =>
acc.filter((lang) => curr.includes(lang)),
);
return this.modules.filter((mod) =>
sharedLangs.includes(mod.language?.toLowerCase()),
);
},
selectOptions() {
return this.modules.map((el) => el.id);
autocompleteOptions() {
return this.compatibleModules.map((mod) => ({
text: mod.id,
value: mod.id,
}));
},
moduleOptions() {
const options = this.selectedItem.options || {};
treeOptions() {
const structure = {};
this.compatibleModules.forEach((mod) => {
const parts = mod.id.split("_");
const language = parts[0] || "unknown";

if (options && options.Agent) {
delete options.Agent;
}
let category = parts[1] || "uncategorized";
if (
parts.length >= 3 &&
["situational", "lateral", "code"].includes(parts[1])
) {
const joined = `${parts[1]}_${parts[2]}`;
if (
[
"situational_awareness",
"lateral_movement",
"code_execution",
].includes(joined)
) {
category = joined;
}
}

if (!structure[language]) structure[language] = {};
if (!structure[language][category]) structure[language][category] = [];
structure[language][category].push({
id: mod.id,
name: mod.id,
});
});

return Object.entries(structure)
.sort(([langA], [langB]) => langA.localeCompare(langB)) // Sort languages
.map(([lang, categories]) => {
const children = Object.entries(categories)
.sort(([catA], [catB]) => catA.localeCompare(catB)) // Sort categories
.flatMap(([cat, modules]) => {
modules.sort((a, b) => a.name.localeCompare(b.name)); // Sort modules

if (modules.length === 1) {
return modules;
}
return {
id: `${lang}_${cat}`,
name: cat,
children: modules,
};
});

return {
id: lang,
name: lang,
children,
};
});
},
moduleOptions() {
const options = this.selectedItem.options || {};
if (options.Agent) delete options.Agent;
Object.keys(this.moduleOptionDefaults || {}).forEach((key) => {
if (options[key]) {
options[key].value = this.moduleOptionDefaults[key];
}
if (options[key]) options[key].value = this.moduleOptionDefaults[key];
});
return options;
},
moduleInfo() {
if (Object.keys(this.selectedItem).length === 0) {
return {};
}

if (Object.keys(this.selectedItem).length === 0) return {};
return {
authors: this.selectedItem.authors,
description: this.selectedItem.description,
Expand Down Expand Up @@ -238,6 +373,9 @@ export default {
await this.moduleStore.getModules();
},
methods: {
toggleTree() {
this.showTreeDialog = true;
},
async handleSelect(item) {
this.errorState = false;
if (!item) {
Expand All @@ -258,9 +396,19 @@ export default {
this.reset = true;
}, 500);
},
handleTreeSelect(selected) {
if (selected.length > 0) {
const moduleId = selected[0];
const match = this.compatibleModules.find((m) => m.id === moduleId);
if (match) {
this.selectedModule = moduleId;
this.handleSelect(moduleId);
this.showTreeDialog = false;
}
}
},
rowClass(item) {
if (item.status === "rejected") return "red";
return "";
return item.status === "rejected" ? "red" : "";
},
emitModuleChange(newVal) {
this.$emit("moduleChange", newVal);
Expand All @@ -269,26 +417,21 @@ export default {
return this.$refs.generalform.$refs.form.validate();
},
async create() {
if (this.agents.length < 1 || this.loading || !this.validate()) {
return;
}

if (this.agents.length < 1 || this.loading || !this.validate()) return;
this.loading = true;

const result = await Promise.allSettled(
this.agents.map((agent) =>
moduleApi.executeModule(
this.selectedModule,
{
...this.form,
Agent: agent,
Agent: agent.session_id,
},
this.ignoreAdminCheck,
this.ignoreLanguageCheck,
),
),
);

if (result.some((item) => item.status === "rejected")) {
const split = result.reduce(
(acc, val) => {
Expand All @@ -297,7 +440,6 @@ export default {
},
{ rejected: [], fulfilled: [] },
);

if (this.agents.length > 1) {
this.$snack.warn(
`Module failed to execute for ${split.rejected.length} out of ${this.agents.length} agents.`,
Expand All @@ -317,15 +459,21 @@ export default {
this.selectedModule = "";
this.$emit("submitted");
}

this.loading = false;
},
},
};
</script>

<style lang="scss" scoped>
<style scoped lang="scss">
.red {
background-color: #bd4c4c;
}

.treeview-wrapper {
border: 1px solid #ccc;
border-radius: 4px;
padding: 10px;
background-color: #454545;
}
</style>
4 changes: 2 additions & 2 deletions src/views/AgentEdit.vue
Original file line number Diff line number Diff line change
Expand Up @@ -153,8 +153,8 @@
<agent-interact :agent="agent" />
<v-divider />
<h4 class="pl-4 pt-2">Execute Module</h4>
<agent-execute-module :agents="[agent.session_id]"
/></v-tab-item>
<agent-execute-module :agents="[agent]" />
</v-tab-item>
<v-tab-item
key="terminal"
style="height: auto"
Expand Down
Loading
Loading