burp-value-autoupdate/src/main/kotlin/UI.kt

847 lines
26 KiB
Kotlin

package burp
import burp.api.montoya.MontoyaApi
import burp.api.montoya.core.ByteArray
import burp.api.montoya.core.ToolType
import burp.api.montoya.core.ToolType.*
import burp.api.montoya.persistence.Preferences
import net.miginfocom.swing.MigLayout
import java.awt.Color
import java.awt.Container
import java.awt.Font
import java.awt.Window
import java.awt.event.*
import javax.swing.*
import javax.swing.JOptionPane.showMessageDialog
import javax.swing.SwingUtilities.getWindowAncestor
import javax.swing.event.TableModelEvent
import javax.swing.table.DefaultTableModel
import javax.swing.table.TableModel
val VALUES_TABLE = JTable().apply {
setSelectionMode(ListSelectionModel.MULTIPLE_INTERVAL_SELECTION)
model = object : DefaultTableModel(
arrayOf(), arrayOf(
"", "Name", "Match", "Last value", "Times updated", "Times replaced", "Transformer"
)
) {
override fun getColumnClass(columnIndex: Int): Class<*> {
if (columnIndex == 0) return Boolean::class.javaObjectType
return String::class.javaObjectType
}
override fun isCellEditable(rowIndex: Int, columnIndex: Int): Boolean {
return columnIndex == 0 // enabled button
}
}
columnModel.getColumn(0).preferredWidth = 25
}
val TRANSFORMER_TABLE = JTable().apply {
setSelectionMode(ListSelectionModel.SINGLE_SELECTION)
model = object : DefaultTableModel(
arrayOf(), arrayOf(
"Name"
)
) {
override fun isCellEditable(rowIndex: Int, columnIndex: Int): Boolean {
return false
}
}
}
const val TAB_NAME = "Value Tracker"
var TAB_VISIBLE = false
var namedRows = mapOf<String, Int>()
var namedTRows = mapOf<String, Int>()
fun updated(name: String, value: String, count: Int) {
if (!TAB_VISIBLE) return
val dtm = VALUES_TABLE.model as DefaultTableModel
namedRows[name]?.let {
dtm.setValueAt(value, it, 3)
dtm.setValueAt(count, it, 4)
}
}
fun replaced(name: String, count: Int) {
if (!TAB_VISIBLE) return
val dtm = VALUES_TABLE.model as DefaultTableModel
namedRows[name]?.let { dtm.setValueAt(count, it, 5) }
}
private fun reloadValuesTable(items: Items) {
val dtm = VALUES_TABLE.model as DefaultTableModel
val tmpMap = mutableMapOf<String, Int>()
VALUES_TABLE.clearSelection()
dtm.dataVector.removeAllElements()
items.forEach {
dtm.addRow(
arrayOf(
it.value.enabled,
it.key,
it.value.match,
it.value.lastMatch,
it.value.matchCount,
it.value.replaceCount,
it.value.transformer
)
)
tmpMap[it.key] = dtm.rowCount - 1
}
dtm.fireTableDataChanged()
namedRows = tmpMap
}
private fun reloadTransformersTable(transformers: Transformers) {
val dtm = TRANSFORMER_TABLE.model as DefaultTableModel
val tmpMap = mutableMapOf<String, Int>()
TRANSFORMER_TABLE.clearSelection()
dtm.dataVector.removeAllElements()
transformers.forEach {
dtm.addRow(
arrayOf(
it.key
)
)
tmpMap[it.key] = dtm.rowCount - 1
}
dtm.fireTableDataChanged()
namedTRows = tmpMap
}
private fun rowToName(row: Int): String {
val dtm = VALUES_TABLE.model
return dtm.getValueAt(row, 1).toString()
}
private fun rowToTName(row: Int): String {
val dtm = TRANSFORMER_TABLE.model
return dtm.getValueAt(row, 0).toString()
}
class UI(api: MontoyaApi, private val itemStore: ItemStore, private val transformerStore: TransformerStore) : JPanel() {
private val ctx: Preferences = api.persistence().preferences()
private var extEnabled = true
private var enabledTools = mutableMapOf(
PROXY to true,
REPEATER to true,
SCANNER to false,
SEQUENCER to true,
INTRUDER to true,
EXTENSIONS to false,
)
private val settingsLabel = JLabel("Settings").apply {
font = font.deriveFont(font.style or Font.BOLD)
}
private val enabledToggle = JCheckBox("Extension enabled").apply {
addItemListener { e: ItemEvent -> enabledToggle(e) }
}
private val headerNestedPanel = JPanel(MigLayout("hidemode 3", "[fill]", "[]")).apply {
add(enabledToggle, "cell 0 0")
}
private val headerPanel = JPanel(MigLayout("hidemode 3", "[fill]", "[][][]")).apply {
add(settingsLabel, "cell 0 0")
add(headerNestedPanel, "cell 0 1")
}
private val separator2 = JSeparator()
private val valuesLabel = JLabel("Values to Track").apply {
font = font.deriveFont(font.style or Font.BOLD)
}
private val valueAdd = JButton("Add").apply {
addActionListener { valueAdd() }
}
private val valueEdit = JButton("Edit").apply {
addActionListener { valueEdit() }
isEnabled = false
}
private val valueRemove = JButton("Remove").apply {
addActionListener { valueRemove() }
isEnabled = false
}
private val valueButtons = JPanel(MigLayout("hidemode 3", "[fill]", "[][][]")).apply {
add(valueAdd, "cell 0 0")
add(valueEdit, "cell 0 1")
add(valueRemove, "cell 0 2")
}
private val valuesTablePanel = JScrollPane(VALUES_TABLE)
private val valueSelectorPanel = JPanel(MigLayout("hidemode 3", "[fill][fill]", "[]")).apply {
add(valueButtons, "cell 0 0,aligny top,growy 0")
add(valuesTablePanel, "cell 1 0,grow,push,span")
}
private val separator1 = JSeparator()
private val toolsLabel = JLabel("Enabled tools").apply {
font = font.deriveFont(font.style or Font.BOLD)
}
private val proxySel = JCheckBox("Proxy").apply {
addItemListener { e: ItemEvent -> toolSelected(PROXY, e) }
}
private val scannerSel = JCheckBox("Scanner").apply {
addItemListener { e: ItemEvent -> toolSelected(SCANNER, e) }
}
private val intruderSel = JCheckBox("Intruder").apply {
addItemListener { e: ItemEvent -> toolSelected(INTRUDER, e) }
}
private val repeaterSel = JCheckBox("Repeater").apply {
addItemListener { e: ItemEvent -> toolSelected(REPEATER, e) }
}
private val sequencerSel = JCheckBox("Sequencer").apply {
addItemListener { e: ItemEvent -> toolSelected(SEQUENCER, e) }
}
private val extenderSel = JCheckBox("Extensions").apply {
addItemListener { e: ItemEvent -> toolSelected(EXTENSIONS, e) }
}
private val toolSelectionMap = mapOf(
PROXY to proxySel,
SCANNER to scannerSel,
INTRUDER to intruderSel,
REPEATER to repeaterSel,
SEQUENCER to sequencerSel,
EXTENSIONS to extenderSel,
)
private val toolSelectionPanel = JPanel(MigLayout("hidemode 3", "[fill][fill][fill]", "[][]")).apply {
add(proxySel, "cell 0 0")
add(scannerSel, "cell 1 0")
add(intruderSel, "cell 2 0")
add(repeaterSel, "cell 0 1")
add(sequencerSel, "cell 1 1")
add(extenderSel, "cell 2 1")
}
private val toolsPanel = JPanel(MigLayout("hidemode 3", "[fill]", "[][]")).apply {
add(toolsLabel, "cell 0 0")
add(toolSelectionPanel, "cell 0 1")
}
private val leftPanel = JPanel(MigLayout("fillx,hidemode 3,align left top", "[fill]", "[][][][][][]")).apply {
add(valuesLabel, "cell 0 0")
add(valueSelectorPanel, "cell 0 1")
add(separator1, "cell 0 2")
add(toolsPanel, "cell 0 3")
add(separator2, "cell 0 4")
add(headerPanel, "cell 0 5")
}
private val transformerLabel = JLabel("Value Transformers").apply {
font = font.deriveFont(font.style or Font.BOLD)
}
private val transformerAdd = JButton("Add").apply {
addActionListener { transformerAdd() }
}
private val transformerRemove = JButton("Remove").apply {
addActionListener { transformerRemove() }
isEnabled = false
}
private val transformerButtons = JPanel(MigLayout("hidemode 3", "[fill]", "[][]")).apply {
add(transformerAdd, "cell 0 0")
add(transformerRemove, "cell 0 1")
}
private val transformerTablePanel = JScrollPane(TRANSFORMER_TABLE)
private val transformerSelectorPanel = JPanel(MigLayout("hidemode 3", "[fill][fill]", "[]")).apply {
add(transformerButtons, "cell 0 0,aligny top,growy")
add(transformerTablePanel, "cell 1 0,grow,push,span")
}
private val transformerEditorSave = JButton("Save").apply {
addActionListener { transformerSave() }
isEnabled = false
}
private val transformerEditorTest = JButton("Test").apply {
addActionListener { transformerTest() }
isEnabled = false
}
private val transformerEditor = api.userInterface().createRawEditor().apply {
(uiComponent() as Container).components.filterNotNull().firstOrNull { it.name == "messageEditor" }
?.let { it as JScrollPane }?.components?.filterIsInstance<JViewport>()?.flatMap { it.components.asList() }
?.firstOrNull { it.name == "syntaxTextArea" }?.let { it as JTextArea }
?.addKeyListener(object : KeyAdapter() {
override fun keyTyped(e: KeyEvent) {
this@UI.editorTyped()
}
})
}
private val transformerEditorPanel = JPanel(MigLayout("hidemode 3", "[]", "[][]")).apply {
isVisible = false
add(transformerEditorSave, "cell 0 0")
add(transformerEditorTest, "cell 1 0")
add(transformerEditor.uiComponent(), "cell 0 1,grow,push,span")
}
private val rightPanel = JPanel(MigLayout("fill,hidemode 1,align left top", "[fill]", "[][][]")).apply {
add(transformerLabel, "cell 0 0")
add(transformerSelectorPanel, "cell 0 1")
add(transformerEditorPanel, "cell 0 2,grow,push,span")
}
private val splitPane = JSplitPane(JSplitPane.HORIZONTAL_SPLIT, leftPanel, rightPanel).apply {
resizeWeight = 0.5
}
init {
layout = MigLayout("fill,hidemode 3,align center top", "fill")
add(splitPane, "w 100%,aligny top,grow,span")
(VALUES_TABLE.model as DefaultTableModel).addTableModelListener { e: TableModelEvent -> tableEdit(e) }
VALUES_TABLE.selectionModel.addListSelectionListener { tableSelected() }
TRANSFORMER_TABLE.selectionModel.addListSelectionListener { transformerSelected() }
addComponentListener(object : ComponentAdapter() {
override fun componentShown(e: ComponentEvent?) {
TAB_VISIBLE = true
reloadValuesTable(itemStore.items)
}
override fun componentHidden(e: ComponentEvent?) {
TAB_VISIBLE = false
}
})
loadValuesFromStore()
api.userInterface().registerSuiteTab(TAB_NAME, this)
}
private fun loadValuesFromStore() {
extEnabled = ctx.getBoolean("extEnabled") ?: true
enabledToggle.isSelected = extEnabled
toolSelectionMap.forEach {
enabledTools[it.key] = ctx.getBoolean("${it.key.name}-enabled") ?: enabledTools[it.key]!!
it.value.isSelected = enabledTools[it.key]!!
}
reloadValuesTable(itemStore.items)
reloadTransformersTable(transformerStore.transformers)
}
fun isEnabled(tool: ToolType): Boolean {
return extEnabled and enabledTools[tool]!!
}
private fun valueAdd() {
val window = AddEditDialog(getWindowAncestor(this), -1, itemStore, transformerStore)
window.title = "Add value"
window.isVisible = true
}
private fun transformerAdd() {
val window = TransformerAddDialog(getWindowAncestor(this), transformerStore)
window.title = "Add transformer"
window.isVisible = true
}
private fun valueEdit() {
val selected = VALUES_TABLE.selectedRowCount
if (selected < 1) {
return
}
if (selected != 1) {
showMessageDialog(null, "Can only edit 1 entry at a time!")
return
}
val index = VALUES_TABLE.selectedRows[0]
val window = AddEditDialog(getWindowAncestor(this), index, itemStore, transformerStore)
window.title = "Edit value"
window.isVisible = true
}
private fun valueRemove() {
val selectedRows = VALUES_TABLE.selectedRows.reversed()
VALUES_TABLE.clearSelection() // visual bug workaround
for (row in selectedRows) {
itemStore.items.remove(rowToName(row))
}
itemStore.save()
reloadValuesTable(itemStore.items)
}
private fun transformerRemove() {
val selectedRows = TRANSFORMER_TABLE.selectedRows
TRANSFORMER_TABLE.clearSelection() // visual bug workaround
if (selectedRows.isEmpty()) return
transformerStore.transformers.remove(rowToTName(selectedRows[0]))
transformerStore.save()
transformerEditor.contents = ByteArray.byteArray("")
reloadTransformersTable(transformerStore.transformers)
}
private fun transformerSave() {
transformerStore.transformers[rowToTName(TRANSFORMER_TABLE.selectedRow)] = transformerEditor.contents.toString()
transformerStore.save()
transformerEditorSave.isEnabled = false
}
private fun transformerTest() {
val window = TransformerTestDialog(
getWindowAncestor(this),
transformerEditor.contents.toString(),
itemStore.items[rowToName(VALUES_TABLE.selectedRow)]?.lastMatch!!
)
window.title = "Transformer output"
window.isVisible = true
}
private fun enabledToggle(e: ItemEvent) {
extEnabled = e.stateChange == ItemEvent.SELECTED
ctx.setBoolean("extEnabled", extEnabled)
}
private fun toolSelected(tool: ToolType, e: ItemEvent) {
val enabled = e.stateChange == ItemEvent.SELECTED
enabledTools[tool] = enabled
ctx.setBoolean("${tool.name}-enabled", enabled)
}
private fun tableEdit(e: TableModelEvent) {
val dtm = e.source as TableModel
if (dtm.rowCount == 0) return
val index = e.firstRow
val enabled = dtm.getValueAt(index, 0) as Boolean
itemStore.items[rowToName(index)]!!.enabled = enabled
itemStore.save()
}
private fun tableSelected() {
when (VALUES_TABLE.selectedRowCount) {
0 -> {
valueRemove.isEnabled = false
valueEdit.isEnabled = false
transformerEditorTest.isEnabled = false
}
1 -> {
valueRemove.isEnabled = true
valueEdit.isEnabled = true
if (TRANSFORMER_TABLE.selectedRowCount == 1) transformerEditorTest.isEnabled = true
}
else -> {
valueRemove.isEnabled = true
valueEdit.isEnabled = false
transformerEditorTest.isEnabled = false
}
}
}
private fun transformerSelected() {
transformerEditorSave.isEnabled = false
when (TRANSFORMER_TABLE.selectedRowCount) {
0 -> {
transformerRemove.isEnabled = false
transformerEditorPanel.isVisible = false
}
else -> {
transformerRemove.isEnabled = true
transformerEditor.contents =
ByteArray.byteArray(transformerStore.transformers[rowToTName(TRANSFORMER_TABLE.selectedRow)]!!)
if (VALUES_TABLE.selectedRowCount == 1) transformerEditorTest.isEnabled = true
transformerEditorPanel.isVisible = true
}
}
}
private fun editorTyped() {
if (TRANSFORMER_TABLE.selectedRowCount != 0) {
transformerEditorSave.isEnabled = true
}
}
}
class AddEditDialog(
owner: Window?,
private var index: Int,
private val itemStore: ItemStore,
private val transformerStore: TransformerStore
) : JDialog(owner) {
private val headerTypeHint = "Matches header names and replaces values "
private val regexTypeHint = "Uses regex for matches (named group: val)"
private val nameLabel = JLabel("Name")
private val nameField = JTextField().apply {
addKeyListener(object : KeyAdapter() {
override fun keyTyped(e: KeyEvent) {
this@AddEditDialog.keyTyped()
}
})
}
private val matchLabel = JLabel("Match")
private val matchField = JTextField().apply {
addKeyListener(object : KeyAdapter() {
override fun keyTyped(e: KeyEvent) {
this@AddEditDialog.keyTyped()
}
})
}
private val typeField = JLabel("Type")
private val headerButton = JRadioButton("Header").apply {
isSelected = true
addItemListener { e: ItemEvent -> headerButtonItemStateChanged(e) }
}
private val regexButton = JRadioButton("Regex").apply {
addItemListener { e: ItemEvent -> regexButtonItemStateChanged(e) }
}
private val typeDescription = JLabel(headerTypeHint)
private val transformerLabel = JLabel("Transformer")
private val transformerComboBox = JComboBox<String>()
private val optionsPanel = JPanel(MigLayout("hidemode 3", "[fill][fill]", "[][][][]")).apply {
add(nameLabel, "cell 0 0")
add(nameField, "cell 1 0,wmin 270,grow 0")
add(matchLabel, "cell 0 1")
add(matchField, "cell 1 1,wmin 270,grow 0")
add(typeField, "cell 0 2")
add(headerButton, "cell 1 2")
add(regexButton, "cell 1 2")
add(typeDescription, "cell 1 3")
add(transformerLabel, "cell 0 4")
add(transformerComboBox, "cell 1 4")
}
private val errorLabel = JLabel().apply {
foreground = Color.RED
text = " "
}
private val okButton = JButton("OK").apply {
background = UIManager.getColor("Button.background")
font = font.deriveFont(font.style or Font.BOLD)
addActionListener { ok() }
}
private val applyButton = JButton("Apply").apply {
isEnabled = false
addActionListener { apply() }
}
private val cancelButton = JButton("Cancel").apply {
addActionListener { cancel() }
}
private val buttonsPanel = JPanel(MigLayout("fillx,hidemode 3", "[fill][fill][fill][fill][fill]", "[fill]")).apply {
add(okButton, "west,gapx null 10")
add(applyButton, "west")
add(cancelButton, "EAST")
}
init {
ButtonGroup().apply {
add(headerButton)
add(regexButton)
}
contentPane.apply {
layout = MigLayout("hidemode 3", "[fill][fill][fill][fill][fill]", "[][][][][][][]")
add(optionsPanel, "cell 0 0")
add(errorLabel, "cell 0 4")
add(buttonsPanel, "cell 0 5")
}
setSize(250, 100)
isResizable = false
pack()
setLocationRelativeTo(owner)
loadTransformers()
if (index != -1) {
loadStoredValues()
}
defaultCloseOperation = DISPOSE_ON_CLOSE
}
private fun loadTransformers() {
transformerComboBox.addItem("")
transformerStore.transformers.forEach {
transformerComboBox.addItem(it.key)
}
transformerComboBox.selectedIndex = 0
}
private fun loadStoredValues() {
val name = rowToName(index)
val item = itemStore.items[name]
if (item == null) {
log.debug("Failed to get name for row $index")
return
}
nameField.text = name
matchField.text = item.match
when (item.type) {
ItemType.HEADER -> headerButton.isSelected = true
ItemType.REGEX -> regexButton.isSelected = true
}
transformerComboBox.selectedItem = item.transformer
nameField.isEnabled = false
}
private fun keyTyped() {
enableApply()
errorLabel.text = " "
}
private fun headerButtonItemStateChanged(e: ItemEvent) {
enableApply()
if (e.stateChange == ItemEvent.SELECTED) {
typeDescription.text = headerTypeHint
}
}
private fun regexButtonItemStateChanged(e: ItemEvent) {
enableApply()
if (e.stateChange == ItemEvent.SELECTED) {
typeDescription.text = regexTypeHint
}
}
private fun enableApply() {
applyButton.isEnabled = true
}
private fun ok() {
if (apply()) this.dispatchEvent(WindowEvent(this, WindowEvent.WINDOW_CLOSING))
}
private fun apply(): Boolean {
applyButton.isEnabled = false
val name = nameField.text
val match = matchField.text
val type = if (regexButton.isSelected) ItemType.REGEX else ItemType.HEADER
val transformer = transformerComboBox.selectedItem as String
if (name == "") {
errorLabel.text = "Name field cannot be left blank!"
return false
}
if (match == "") {
errorLabel.text = "Match field cannot be left blank!"
return false
}
if (type == ItemType.REGEX) {
val err = checkRegexSyntax(match)
if (err != "") {
errorLabel.text = err
return false
}
}
if (index == -1) {
val item = Item(match, type, "", true, 0, 0, transformer)
if (itemStore.items[name] != null) {
errorLabel.text = "Entry named \"$name\" already exists!"
return false
}
itemStore.items[name] = item
index = VALUES_TABLE.rowCount
log.debug("New item at index $index: $item")
} else {
val item = itemStore.items[rowToName(index)]
if (item == null) {
log.debug("Failed to get name for row $index")
return false
}
item.match = match
item.type = type
item.transformer = transformer
log.debug("Edited item at index $index: $item")
}
nameField.isEnabled = false
itemStore.save()
reloadValuesTable(itemStore.items)
return true
}
private fun cancel() {
this.dispatchEvent(WindowEvent(this, WindowEvent.WINDOW_CLOSING))
}
}
class TransformerAddDialog(owner: Window?, private val transformerStore: TransformerStore) : JDialog(owner) {
private val nameLabel = JLabel("Name")
private val nameField = JTextField().apply {
addKeyListener(object : KeyAdapter() {
override fun keyTyped(e: KeyEvent) {
showError(" ")
}
})
}
private val namePanel = JPanel(MigLayout("hidemode 3", "[fill][fill]", "[][][][]")).apply {
add(nameLabel, "cell 0 0")
add(nameField, "cell 1 0,wmin 270,grow 0")
}
private val errorLabel = JLabel(" ").apply {
foreground = Color.RED
}
private val okButton = JButton("OK").apply {
background = UIManager.getColor("Button.background")
font = font.deriveFont(font.style or Font.BOLD)
addActionListener { if (apply()) close() }
}
private val cancelButton = JButton("Cancel").apply {
addActionListener { close() }
}
private val buttonPanel = JPanel(MigLayout("fillx,hidemode 3", "[fill][fill][fill][fill][fill]", "[fill]")).apply {
add(okButton, "west,gapx null 10")
add(cancelButton, "EAST")
}
init {
contentPane.apply {
layout = MigLayout("hidemode 3", "[fill][fill][fill][fill][fill]", "[][][][][][][]")
add(namePanel, "cell 0 0")
add(errorLabel, "cell 0 4")
add(buttonPanel, "cell 0 5")
}
setSize(250, 100)
isResizable = false
pack()
setLocationRelativeTo(owner)
defaultCloseOperation = DISPOSE_ON_CLOSE
}
private fun close() {
dispatchEvent(WindowEvent(this, WindowEvent.WINDOW_CLOSING))
}
private fun apply(): Boolean {
val name = nameField.text
if (name == "") {
showError("Name field cannot be left blank!")
return false
}
if (transformerStore.transformers[name] != null) {
showError("Transformer named \"$name\" already exists!")
return false
}
transformerStore.transformers[name] = ""
transformerStore.save()
reloadTransformersTable(transformerStore.transformers)
return true
}
private fun showError(err: String) {
errorLabel.text = err
}
}
class TransformerTestDialog(owner: Window?, transformer: String, value: String) : JDialog(owner) {
private val nameLabel = JLabel("Output")
private val output = JTextArea(10, 80).apply {
font = Font(Font.MONOSPACED, Font.PLAIN, 12)
isEditable = false
lineWrap = true
val transformed = evalTransformer(value, transformer)
if (transformed.err == "") {
text = transformed.out
} else {
nameLabel.text = "Error"
nameLabel.foreground = Color.RED
text = transformed.err
}
}
private val okButton = JButton("OK").apply {
background = UIManager.getColor("Button.background")
font = font.deriveFont(font.style or Font.BOLD)
addActionListener {
this@TransformerTestDialog.dispatchEvent(
WindowEvent(
this@TransformerTestDialog, WindowEvent.WINDOW_CLOSING
)
)
}
}
private val buttonPanel = JPanel(MigLayout("fillx,hidemode 3", "[fill][fill][fill][fill][fill]", "[fill]")).apply {
add(okButton, "west,gapx null 10")
}
init {
contentPane.apply {
layout = MigLayout("hidemode 3", "[fill][fill][fill][fill][fill]", "[][][][][][][]")
add(nameLabel, "cell 0 0, wrap")
add(JScrollPane(output), "grow, push, span")
add(buttonPanel, "cell 0 4")
}
pack()
setLocationRelativeTo(owner)
defaultCloseOperation = DISPOSE_ON_CLOSE
}
}