/* * Zed Attack Proxy (ZAP) and its related class files. * * ZAP is an HTTP/HTTPS proxy for assessing web application security. * * Copyright 2011 The ZAP Development team * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.zaproxy.zap.extension.alert; import java.awt.CardLayout; import java.awt.Component; import java.awt.Dimension; import java.awt.Event; import java.awt.EventQueue; import java.awt.GridBagConstraints; import java.awt.GridBagLayout; import java.awt.Point; import java.awt.Toolkit; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.util.ArrayList; import java.util.Collections; import java.util.SortedSet; import java.util.TreeSet; import javax.swing.ImageIcon; import javax.swing.JButton; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JToggleButton; import javax.swing.JToolBar; import javax.swing.JTree; import javax.swing.KeyStroke; import javax.swing.SwingUtilities; import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.TreeNode; import javax.swing.tree.TreePath; import org.apache.log4j.Logger; import org.parosproxy.paros.Constant; import org.parosproxy.paros.control.Control; import org.parosproxy.paros.core.scanner.Alert; import org.parosproxy.paros.extension.AbstractPanel; import org.parosproxy.paros.extension.ViewDelegate; import org.parosproxy.paros.extension.history.ExtensionHistory; import org.parosproxy.paros.model.HistoryReference; import org.parosproxy.paros.model.SiteNode; import org.parosproxy.paros.network.HttpMessage; import org.zaproxy.zap.extension.httppanel.HttpPanel; import org.zaproxy.zap.extension.search.SearchMatch; import org.zaproxy.zap.utils.DisplayUtils; import org.zaproxy.zap.view.DeselectableButtonGroup; import org.zaproxy.zap.view.LayoutHelper; import org.zaproxy.zap.view.ZapToggleButton; import org.zaproxy.zap.view.messagecontainer.http.DefaultSelectableHistoryReferencesContainer; import org.zaproxy.zap.view.messagecontainer.http.SelectableHistoryReferencesContainer; public class AlertPanel extends AbstractPanel { public static final String ALERT_TREE_PANEL_NAME = "treeAlert"; private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(AlertPanel.class); private ViewDelegate view = null; private JTree treeAlert = null; private JScrollPane paneScroll = null; private JPanel panelCommand = null; private JToolBar panelToolbar = null; private JSplitPane splitPane = null; private AlertViewPanel alertViewPanel = null; private ZapToggleButton scopeButton = null; /** * The {@code AlertTreeModel} that holds the alerts of a selected "Sites" tree node when the filter * "Link with Sites selection" is enabled, otherwise it will be empty. * <p> * The tree model is lazily initialised (in the method {@code getLinkWithSitesTreeModel()}). * </p> * * @see #getLinkWithSitesTreeModel() * @see #setLinkWithSitesTreeSelection(boolean) */ private AlertTreeModel linkWithSitesTreeModel; /** * The tree selection listener that triggers the filter "Link with Sites selection" of the "Alerts" tree based on selected * "Sites" tree node. * <p> * The listener is lazily initialised (in the method {@code getLinkWithSitesTreeSelectionListener()}). * </p> * * @see #getLinkWithSitesTreeSelectionListener() * @see #setLinkWithSitesTreeSelection(boolean) */ private LinkWithSitesTreeSelectionListener linkWithSitesTreeSelectionListener; /** * The toggle button that enables/disables the "Alerts" tree filtering based on the "Sites" tree selection. * <p> * The toggle button is lazily initialised (in the method {@code getLinkWithSitesTreeButton()}). * </p> * * @see #getLinkWithSitesTreeButton() */ private ZapToggleButton linkWithSitesTreeButton; /** * The button group used to control mutually exclusive "Alerts" tree filtering buttons. * <p> * Any filtering buttons that are mutually exclusive should be added to this button group. * </p> */ private DeselectableButtonGroup alertsTreeFiltersButtonGroup; private JButton editButton = null; private ExtensionAlert extension = null; private ExtensionHistory extHist = null; public AlertPanel(ExtensionAlert extension) { super(); this.extension = extension; alertsTreeFiltersButtonGroup = new DeselectableButtonGroup(); initialize(); } /** * This method initializes this */ private void initialize() { this.setLayout(new CardLayout()); this.setSize(274, 251); this.setName(Constant.messages.getString("alerts.panel.title")); this.setIcon(new ImageIcon(AlertPanel.class.getResource("/resource/icon/16/071.png"))); // 'flag' icon this.add(getPanelCommand(), getPanelCommand().getName()); this.setDefaultAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | Event.SHIFT_MASK, false)); this.setMnemonic(Constant.messages.getChar("alerts.panel.mnemonic")); this.setShowByDefault(true); } private javax.swing.JPanel getPanelCommand() { if (panelCommand == null) { panelCommand = new javax.swing.JPanel(); panelCommand.setLayout(new java.awt.GridBagLayout()); panelCommand.setName("AlertPanel"); GridBagConstraints gridBagConstraints1 = new GridBagConstraints(); GridBagConstraints gridBagConstraints2 = new GridBagConstraints(); gridBagConstraints1.gridx = 0; gridBagConstraints1.gridy = 0; gridBagConstraints1.weightx = 1.0D; gridBagConstraints1.insets = new java.awt.Insets(2,2,2,2); gridBagConstraints1.fill = java.awt.GridBagConstraints.HORIZONTAL; gridBagConstraints1.anchor = java.awt.GridBagConstraints.NORTHWEST; gridBagConstraints2.gridx = 0; gridBagConstraints2.gridy = 1; gridBagConstraints2.weightx = 1.0; gridBagConstraints2.weighty = 1.0; gridBagConstraints2.insets = new java.awt.Insets(0,0,0,0); gridBagConstraints2.fill = java.awt.GridBagConstraints.BOTH; gridBagConstraints2.anchor = java.awt.GridBagConstraints.NORTHWEST; panelCommand.add(getSplitPane(), gridBagConstraints2); } return panelCommand; } private javax.swing.JToolBar getPanelToolbar() { if (panelToolbar == null) { panelToolbar = new javax.swing.JToolBar(); panelToolbar.setEnabled(true); panelToolbar.setFloatable(false); panelToolbar.setRollover(true); panelToolbar.setPreferredSize(new java.awt.Dimension(800,30)); panelToolbar.setName("AlertToolbar"); panelToolbar.add(getScopeButton()); panelToolbar.add(getLinkWithSitesTreeButton()); panelToolbar.addSeparator(); panelToolbar.add(getEditButton()); } return panelToolbar; } private JSplitPane getSplitPane() { if (splitPane == null) { splitPane = new JSplitPane(); splitPane.setName("AlertPanels"); splitPane.setDividerSize(3); splitPane.setDividerLocation(400); splitPane.setOrientation(JSplitPane.HORIZONTAL_SPLIT); // Add toolbar JPanel panel = new JPanel(); panel.setLayout(new GridBagLayout()); panel.add(this.getPanelToolbar(), LayoutHelper.getGBC(0, 0, 1, 0.0D)); panel.add(getPaneScroll(), LayoutHelper.getGBC(0, 1, 1, 1.0D, 1.0D)); splitPane.setLeftComponent(panel); splitPane.setRightComponent(getAlertViewPanel()); splitPane.setPreferredSize(new Dimension(100,200)); } return splitPane; } public AlertViewPanel getAlertViewPanel() { if (alertViewPanel == null) { alertViewPanel = new AlertViewPanel(); } return alertViewPanel; } private JToggleButton getScopeButton() { if (scopeButton == null) { scopeButton = new ZapToggleButton(); scopeButton.setIcon(new ImageIcon(AlertPanel.class.getResource("/resource/icon/fugue/target-grey.png"))); scopeButton.setToolTipText(Constant.messages.getString("history.scope.button.unselected")); scopeButton.setSelectedIcon(new ImageIcon(AlertPanel.class.getResource("/resource/icon/fugue/target.png"))); scopeButton.setSelectedToolTipText(Constant.messages.getString("history.scope.button.selected")); DisplayUtils.scaleIcon(scopeButton); scopeButton.addActionListener(new java.awt.event.ActionListener() { @Override public void actionPerformed(java.awt.event.ActionEvent e) { extension.setShowJustInScope(scopeButton.isSelected()); } }); alertsTreeFiltersButtonGroup.add(scopeButton); } return scopeButton; } /** * Returns the toggle button that enables/disables the "Alerts" tree filtering based on the "Sites" tree selection. * <p> * The instance variable {@code linkWithSitesTreeButton} is initialised on the first call to this method. * </p> * * @see #linkWithSitesTreeButton * @return the toggle button that enables/disables the "Alerts" tree filtering based on the "Sites" tree selection */ private JToggleButton getLinkWithSitesTreeButton() { if (linkWithSitesTreeButton == null) { linkWithSitesTreeButton = new ZapToggleButton(); linkWithSitesTreeButton.setIcon(new ImageIcon(AlertPanel.class.getResource("/resource/icon/16/earth-grey.png"))); linkWithSitesTreeButton.setToolTipText(Constant.messages.getString("alerts.panel.linkWithSitesSelection.unselected.button.tooltip")); linkWithSitesTreeButton.setSelectedIcon(new ImageIcon(AlertPanel.class.getResource("/resource/icon/16/094.png"))); linkWithSitesTreeButton.setSelectedToolTipText(Constant.messages.getString("alerts.panel.linkWithSitesSelection.selected.button.tooltip")); DisplayUtils.scaleIcon(linkWithSitesTreeButton); linkWithSitesTreeButton.addActionListener(new java.awt.event.ActionListener() { @Override public void actionPerformed(java.awt.event.ActionEvent e) { setLinkWithSitesTreeSelection(linkWithSitesTreeButton.isSelected()); } }); alertsTreeFiltersButtonGroup.add(linkWithSitesTreeButton); } return linkWithSitesTreeButton; } /** * Sets whether the "Alerts" tree is filtered, or not based on a selected "Sites" tree node. * <p> * If {@code enabled} is {@code true} only the alerts of the selected "Sites" tree node will be shown. * </p> * <p> * Calling this method removes the filter "Just in Scope", if enabled, as they are mutually exclusive. * </p> * * @param enabled {@code true} if the "Alerts" tree should be filtered based on the "Sites" tree selection, {@code false} * otherwise. * @see ExtensionAlert#setShowJustInScope(boolean) */ public void setLinkWithSitesTreeSelection(boolean enabled) { linkWithSitesTreeButton.setSelected(enabled); if (enabled) { extension.setShowJustInScope(false); final JTree sitesTree = view.getSiteTreePanel().getTreeSite(); final TreePath selectionPath = sitesTree.getSelectionPath(); getTreeAlert().setModel(getLinkWithSitesTreeModel()); if (selectionPath != null) { recreateLinkWithSitesTreeModel((SiteNode) selectionPath.getLastPathComponent()); } sitesTree.addTreeSelectionListener(getLinkWithSitesTreeSelectionListener()); } else { extension.setMainTreeModel(); ((AlertNode) getLinkWithSitesTreeModel().getRoot()).removeAllChildren(); view.getSiteTreePanel().getTreeSite().removeTreeSelectionListener(getLinkWithSitesTreeSelectionListener()); } } /** * Returns the {@code AlertTreeModel} that holds the alerts of the selected "Sites" tree node when the filter * "Link with Sites selection" is enabled, otherwise it will be empty. * <p> * The instance variable {@code linkWithSitesTreeModel} is initialised on the first call to this method. * </p> * * @see #linkWithSitesTreeModel * @return the {@code AlertTreeModel} that holds the alerts of the selected "Sites" tree node when the filter * "Link with Sites selection" is enabled, otherwise it will be empty */ private AlertTreeModel getLinkWithSitesTreeModel() { if (linkWithSitesTreeModel == null) { linkWithSitesTreeModel = new AlertTreeModel(); } return linkWithSitesTreeModel; } /** * Recreates the {@code linkWithSitesTreeModel} with the alerts of the given {@code siteNode}. * <p> * If the given {@code siteNode} doesn't contain any alerts the resulting model will only contain the root node, otherwise * the model will contain the root node and the alerts returned by the method {@code SiteNode#getAlerts()} although if the * node has an HistoryReference only the alerts whose URI is equal to the URI returned by the method * {@code HistoryReference#getURI()} will be included. * </p> * <p> * After a call to this method the number of total alerts will be recalculated by calling the method * {@code ExtensionAlert#recalcAlerts()}. * </p> * * @param siteNode the "Sites" tree node that will be used to recreate the alerts tree model. * @throws IllegalArgumentException if {@code siteNode} is {@code null}. * @see #linkWithSitesTreeModel * @see #setLinkWithSitesTreeSelection * @see Alert * @see ExtensionAlert#recalcAlerts() * @see HistoryReference * @see SiteNode#getAlerts() */ private void recreateLinkWithSitesTreeModel(SiteNode siteNode) { if (siteNode == null) { throw new IllegalArgumentException("Parameter siteNode must not be null."); } ((AlertNode) getLinkWithSitesTreeModel().getRoot()).removeAllChildren(); if (siteNode.isRoot()) { getLinkWithSitesTreeModel().reload(); extension.recalcAlerts(); return; } String uri = null; HistoryReference historyReference = siteNode.getHistoryReference(); if (historyReference != null) { uri = historyReference.getURI().toString(); } for (Alert alert : siteNode.getAlerts()) { // Just show ones for this node if (uri != null && !alert.getUri().equals(uri)) { continue; } getLinkWithSitesTreeModel().addPath(alert); } getLinkWithSitesTreeModel().reload(); expandRootChildNodes(); extension.recalcAlerts(); } /** * Returns the tree selection listener that triggers the filter of the "Alerts" tree based on a selected {@code SiteNode}. * <p> * The instance variable {@code linkWithSitesTreeSelectionListener} is initialised on the first call to this method. * </p> * * @see #linkWithSitesTreeSelectionListener * @return the tree selection listener that triggers the filter of the "Alerts" tree based on a selected {@code SiteNode}. */ private LinkWithSitesTreeSelectionListener getLinkWithSitesTreeSelectionListener() { if (linkWithSitesTreeSelectionListener == null) { linkWithSitesTreeSelectionListener = new LinkWithSitesTreeSelectionListener(); } return linkWithSitesTreeSelectionListener; } /** * This method initializes treeAlert * * @return javax.swing.JTree */ JTree getTreeAlert() { if (treeAlert == null) { treeAlert = new JTree() { private static final long serialVersionUID = 1L; @Override public Point getPopupLocation(final MouseEvent event) { if (event != null) { // Select item on right click TreePath tp = treeAlert.getPathForLocation(event.getX(), event.getY()); if (tp != null) { // Only select a new item if the current item is not // already selected - this is to allow multiple items // to be selected if (!treeAlert.getSelectionModel().isPathSelected(tp)) { treeAlert.getSelectionModel().setSelectionPath(tp); } } } return super.getPopupLocation(event); } }; treeAlert.setName(ALERT_TREE_PANEL_NAME); treeAlert.setShowsRootHandles(true); treeAlert.setBorder(javax.swing.BorderFactory.createEmptyBorder(0,0,0,0)); treeAlert.setComponentPopupMenu(new JPopupMenu() { private static final long serialVersionUID = 1L; @Override public void show(Component invoker, int x, int y) { final int countSelectedNodes = treeAlert.getSelectionCount(); final ArrayList<HistoryReference> uniqueHistoryReferences = new ArrayList<>(countSelectedNodes); if (countSelectedNodes > 0) { SortedSet<Integer> historyReferenceIdsAdded = new TreeSet<>(); for (TreePath path : treeAlert.getSelectionPaths()) { final AlertNode node = (AlertNode) path.getLastPathComponent(); final Object userObject = node.getUserObject(); if (userObject instanceof Alert) { HistoryReference historyReference = ((Alert) userObject).getHistoryRef(); if (historyReference != null && !historyReferenceIdsAdded .contains(Integer.valueOf(historyReference.getHistoryId()))) { historyReferenceIdsAdded.add(Integer.valueOf(historyReference.getHistoryId())); uniqueHistoryReferences.add(historyReference); } } } uniqueHistoryReferences.trimToSize(); } SelectableHistoryReferencesContainer messageContainer = new DefaultSelectableHistoryReferencesContainer( treeAlert.getName(), treeAlert, Collections.<HistoryReference> emptyList(), uniqueHistoryReferences); view.getPopupMenu().show(messageContainer, x, y); } }); treeAlert.addMouseListener(new java.awt.event.MouseAdapter() { @Override public void mouseClicked(java.awt.event.MouseEvent e) { if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() > 1) { // Its a double click - edit the alert editSelectedAlert(); } } }); treeAlert.addTreeSelectionListener(new javax.swing.event.TreeSelectionListener() { @Override public void valueChanged(javax.swing.event.TreeSelectionEvent e) { DefaultMutableTreeNode node = (DefaultMutableTreeNode) treeAlert.getLastSelectedPathComponent(); if (node != null && node.getUserObject() != null) { Object obj = node.getUserObject(); if (obj instanceof Alert) { Alert alert = (Alert) obj; setMessage(alert.getMessage(), alert.getEvidence()); treeAlert.requestFocusInWindow(); getAlertViewPanel().displayAlert(alert); } else { getAlertViewPanel().clearAlert(); } } else { getAlertViewPanel().clearAlert(); } } }); treeAlert.setCellRenderer(new AlertTreeCellRenderer()); treeAlert.setExpandsSelectedPaths(true); } return treeAlert; } /** * This method initializes paneScroll * * @return javax.swing.JScrollPane */ private JScrollPane getPaneScroll() { if (paneScroll == null) { paneScroll = new JScrollPane(); paneScroll.setName("paneScroll"); paneScroll.setViewportView(getTreeAlert()); } return paneScroll; } void setView(ViewDelegate view) { this.view = view; } /** * @return Returns the view. */ private ViewDelegate getView() { return view; } public void expandRoot() { if (EventQueue.isDispatchThread()) { getTreeAlert().expandPath(new TreePath(getTreeAlert().getModel().getRoot())); return; } try { EventQueue.invokeLater(new Runnable() { @Override public void run() { expandRoot(); } }); } catch (Exception e) { logger.error(e.getMessage(), e); } } /** * Expands all child nodes of the root node. * <p> * This method should be called only from the EDT (Event Dispatch Thread). * </p> */ public void expandRootChildNodes() { TreeNode root = (TreeNode) getTreeAlert().getModel().getRoot(); if (root == null) { return; } TreePath basePath = new TreePath(root); for (int i = 0; i < root.getChildCount(); ++i) { getTreeAlert().expandPath(basePath.pathByAddingChild(root.getChildAt(i))); } } private void setMessage(HttpMessage msg, String highlight) { getView().displayMessage(msg); if (msg == null) { return; } if (!msg.getResponseHeader().isEmpty()) { HttpPanel requestPanel = getView().getRequestPanel(); HttpPanel responsePanel = getView().getResponsePanel(); SearchMatch sm = null; int start; // Highlight the 'attack' / evidence if (highlight == null || highlight.length() == 0) { // ignore } else if ((start = msg.getResponseHeader().toString().indexOf(highlight)) >=0) { sm = new SearchMatch(msg, SearchMatch.Location.RESPONSE_HEAD, start, start + highlight.length()); responsePanel.highlightHeader(sm); responsePanel.setTabFocus(); } else if ((start = msg.getResponseBody().toString().indexOf(highlight)) >=0) { sm = new SearchMatch(msg, SearchMatch.Location.RESPONSE_BODY, start, start + highlight.length()); responsePanel.highlightBody(sm); responsePanel.setTabFocus(); } else if ((start = msg.getRequestHeader().toString().indexOf(highlight)) >=0) { sm = new SearchMatch(msg, SearchMatch.Location.REQUEST_HEAD, start, start + highlight.length()); requestPanel.highlightHeader(sm); requestPanel.setTabFocus(); } else if ((start = msg.getRequestBody().toString().indexOf(highlight)) >=0) { sm = new SearchMatch(msg, SearchMatch.Location.REQUEST_BODY, start, start + highlight.length()); requestPanel.highlightBody(sm); requestPanel.setTabFocus(); } } } /** * The tree selection listener that triggers the filter of the "Alerts" tree by calling the method * {@code recreateLinkWithSitesTreeModel(SiteNode)} with the selected {@code SiteNode} as parameter. * * @see #recreateLinkWithSitesTreeModel(SiteNode) */ private class LinkWithSitesTreeSelectionListener implements TreeSelectionListener { @Override public void valueChanged(TreeSelectionEvent e) { recreateLinkWithSitesTreeModel((SiteNode) e.getPath().getLastPathComponent()); } } private void editSelectedAlert() { DefaultMutableTreeNode node = (DefaultMutableTreeNode) treeAlert.getLastSelectedPathComponent(); if (node != null && node.getUserObject() != null) { Object obj = node.getUserObject(); if (obj instanceof Alert) { Alert alert = (Alert) obj; if (extHist == null) { extHist = (ExtensionHistory) Control.getSingleton().getExtensionLoader().getExtension(ExtensionHistory.NAME); } if (extHist != null) { extHist.showAlertAddDialog(alert); } } } } private JButton getEditButton() { if (editButton == null) { editButton = new JButton(); editButton.setToolTipText(Constant.messages.getString("alert.edit.button.tooltip")); editButton.setIcon(new ImageIcon(AlertPanel.class.getResource("/resource/icon/16/018.png"))); editButton.addActionListener(new ActionListener() { @Override public void actionPerformed(ActionEvent e) { editSelectedAlert(); } }); } return editButton; } }