/*
* Zed Attack Proxy (ZAP) and its related class files.
*
* ZAP is an HTTP/HTTPS proxy for assessing web application security.
*
* Copyright 2013 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.ascan;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.awt.Frame;
import java.awt.GridBagLayout;
import java.awt.Point;
import java.awt.HeadlessException;
import java.awt.Toolkit;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.text.MessageFormat;
import java.util.ArrayList;
import java.util.List;
import javax.swing.Box;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JProgressBar;
import javax.swing.JScrollPane;
import javax.swing.JTabbedPane;
import javax.swing.JTable;
import javax.swing.LookAndFeel;
import javax.swing.SwingUtilities;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableModel;
import org.apache.log4j.Logger;
import org.jdesktop.swingx.plaf.basic.core.BasicTransferable;
import org.jfree.chart.ChartFactory;
import org.jfree.chart.ChartPanel;
import org.jfree.chart.JFreeChart;
import org.jfree.chart.annotations.XYTextAnnotation;
import org.jfree.chart.axis.ValueAxis;
import org.jfree.chart.plot.ValueMarker;
import org.jfree.chart.plot.XYPlot;
import org.jfree.data.time.Second;
import org.jfree.data.time.TimeSeries;
import org.jfree.data.time.TimeSeriesCollection;
import org.jfree.data.xy.XYDataset;
import org.jfree.ui.Layer;
import org.jfree.ui.TextAnchor;
import org.parosproxy.paros.Constant;
import org.parosproxy.paros.core.scanner.HostProcess;
import org.parosproxy.paros.core.scanner.Plugin;
import org.parosproxy.paros.extension.AbstractDialog;
import org.parosproxy.paros.view.View;
import org.zaproxy.zap.utils.FontUtils;
import org.zaproxy.zap.view.LayoutHelper;
/**
* Dialog reviewed for a new lifestyle...
* @author yhawke (2014)
*/
public class ScanProgressDialog extends AbstractDialog {
private static final long serialVersionUID = 1L;
private static Logger log = Logger.getLogger(ScanProgressDialog.class);
private transient Color JTABLE_ALTERNATE_BACKGROUND = (Color)LookAndFeel.getDesktopPropertyValue("Table.alternateRowColor", new Color(0xf2f2f2));
private ExtensionActiveScan extension;
private JScrollPane jScrollPane;
private JTable table;
private ScanProgressTableModel model;
private JButton closeButton;
private JButton copyToClipboardButton;
private JComboBox<String> hostSelect;
private ActiveScan scan;
private boolean stopThread;
private JFreeChart chart;
private List<String> labelsAdded = new ArrayList<String>();
private TimeSeries seriesTotal;
private TimeSeries series100;
private TimeSeries series200;
private TimeSeries series300;
private TimeSeries series400;
private TimeSeries series500;
private double lastCentre = -1;
/**
* Constructs a modal {@code ScanProgressDialog} with the given owner, target and active scan extension.
*
* @param owner the {@code Frame} from which the dialog is displayed
* @param target the scan target, shown as title if not {@code null}
* @param extension the active scan extension, to obtain chart options
* @throws HeadlessException when {@code GraphicsEnvironment.isHeadless()} returns {@code true}
*/
public ScanProgressDialog(Frame owner, String target, ExtensionActiveScan extension) {
super(owner, false);
if (target != null) {
this.setTitle(MessageFormat.format(Constant.messages.getString("ascan.progress.title"), target));
}
this.extension = extension;
this.initialize();
}
private void initialize() {
this.setSize(new Dimension(580, 504));
JTabbedPane tabbedPane = new JTabbedPane();
JPanel tab1 = new JPanel();
tab1.setLayout(new GridBagLayout());
JPanel hostPanel = new JPanel();
hostPanel.setLayout(new GridBagLayout());
hostPanel.add(new JLabel(Constant.messages.getString("ascan.progress.label.host")), LayoutHelper.getGBC(0, 0, 1, 0.4D));
hostPanel.add(getHostSelect(), LayoutHelper.getGBC(1, 0, 1, 0.6D));
tab1.add(hostPanel, LayoutHelper.getGBC(0, 0, 3, 1.0D, 0.0D));
tab1.add(getJScrollPane(), LayoutHelper.getGBC(0, 1, 3, 1.0D, 1.0D));
JPanel buttonsPanel = new JPanel(new FlowLayout(FlowLayout.CENTER, 5, 0));
buttonsPanel.add(getCopyToClipboardButton());
buttonsPanel.add(getCloseButton());
tab1.add(buttonsPanel, LayoutHelper.getGBC(0, 2, 3, 1.0D));
tabbedPane.insertTab(Constant.messages.getString("ascan.progress.tab.progress"), null, tab1, null, 0);
this.add(tabbedPane);
int mins = extension.getScannerParam().getMaxChartTimeInMins();
if (mins > 0) {
// Treat zero mins as disabled
JPanel tab2 = new JPanel();
tab2.setLayout(new GridBagLayout());
this.seriesTotal = new TimeSeries("TotalResponses"); // Name not shown, so no need to i18n
final TimeSeriesCollection dataset = new TimeSeriesCollection(this.seriesTotal);
this.series100 = new TimeSeries(Constant.messages.getString("ascan.progress.chart.1xx"));
this.series200 = new TimeSeries(Constant.messages.getString("ascan.progress.chart.2xx"));
this.series300 = new TimeSeries(Constant.messages.getString("ascan.progress.chart.3xx"));
this.series400 = new TimeSeries(Constant.messages.getString("ascan.progress.chart.4xx"));
this.series500 = new TimeSeries(Constant.messages.getString("ascan.progress.chart.5xx"));
long maxAge = mins * 60;
this.seriesTotal.setMaximumItemAge(maxAge);
this.series100.setMaximumItemAge(maxAge);
this.series200.setMaximumItemAge(maxAge);
this.series300.setMaximumItemAge(maxAge);
this.series400.setMaximumItemAge(maxAge);
this.series500.setMaximumItemAge(maxAge);
dataset.addSeries(series100);
dataset.addSeries(series200);
dataset.addSeries(series300);
dataset.addSeries(series400);
dataset.addSeries(series500);
chart = createChart(dataset);
// Set up some vaguesly sensible colours
chart.getXYPlot().getRenderer(0).setSeriesPaint(0, Color.BLACK); // Totals
chart.getXYPlot().getRenderer(0).setSeriesPaint(1, Color.GRAY); // 100: Info
chart.getXYPlot().getRenderer(0).setSeriesPaint(2, Color.GREEN); // 200: OK
chart.getXYPlot().getRenderer(0).setSeriesPaint(3, Color.BLUE); // 300: Info
chart.getXYPlot().getRenderer(0).setSeriesPaint(4, Color.MAGENTA); // 400: Bad req
chart.getXYPlot().getRenderer(0).setSeriesPaint(5, Color.RED); // 500: Internal error
final ChartPanel chartPanel = new ChartPanel(chart);
tab2.add(chartPanel, LayoutHelper.getGBC(0, 0, 1, 1.0D, 1.0D));
tabbedPane.insertTab(Constant.messages.getString("ascan.progress.tab.chart"), null, tab2, null, 1);
}
// Stop the updating thread when the window is closed
this.addWindowListener(new WindowAdapter() {
@Override
public void windowClosed(WindowEvent e) {
stopThread = true;
}
});
}
private JFreeChart createChart(final XYDataset dataset) {
JFreeChart result = ChartFactory.createTimeSeriesChart(
null, // No title - it just takes up space
Constant.messages.getString("ascan.progress.chart.time"),
Constant.messages.getString("ascan.progress.chart.responses"),
dataset,
true,
true,
false
);
XYPlot plot = result.getXYPlot();
ValueAxis daxis = plot.getDomainAxis();
daxis.setAutoRange(true);
daxis.setAutoRangeMinimumSize(60000.0);
plot.getRangeAxis().setAutoRangeMinimumSize(20);
return result;
}
/**
* Get the dialog scroll panel
* @return the panel
*/
private JScrollPane getJScrollPane() {
if (jScrollPane == null) {
jScrollPane = new JScrollPane();
jScrollPane.setViewportView(getMainPanel());
jScrollPane.setName("ScanProgressScrollPane");
jScrollPane.setHorizontalScrollBarPolicy(JScrollPane.HORIZONTAL_SCROLLBAR_NEVER);
jScrollPane.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_AS_NEEDED);
}
return jScrollPane;
}
private JButton getCloseButton() {
// Note that on Linux dialogs dont get close buttons on the frame decoration
if (closeButton == null) {
closeButton = new JButton(Constant.messages.getString("all.button.close"));
closeButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
dispatchEvent(new WindowEvent(ScanProgressDialog.this, WindowEvent.WINDOW_CLOSING));
}});
}
return closeButton;
}
private JButton getCopyToClipboardButton() {
if (copyToClipboardButton == null) {
copyToClipboardButton = new JButton(Constant.messages.getString("ascan.progress.copyclipboard.button.label"));
copyToClipboardButton.setToolTipText(Constant.messages.getString("ascan.progress.copyclipboard.button.tooltip"));
copyToClipboardButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent evt) {
// Mimics the implementation of BasicTableUI.TableTransferHandler.createTransferable(JComponent) but copies
// all rows (including column names), not just selected rows/columns (which are none in this case).
StringBuilder plainContent = new StringBuilder();
StringBuilder htmlContent = new StringBuilder();
htmlContent.append("<html>\n<body>\n<table>\n");
TableModel tableModel = getMainPanel().getModel();
htmlContent.append("<tr>\n");
for (int col = 0; col < tableModel.getColumnCount(); col++) {
String val = tableModel.getColumnName(col);
plainContent.append(val).append('\t');
htmlContent.append(" <td>").append(val).append("</td>\n");
}
plainContent.deleteCharAt(plainContent.length() - 1).append("\n");
htmlContent.append("</tr>\n");
for (int row = 0; row < tableModel.getRowCount(); row++) {
htmlContent.append("<tr>\n");
for (int col = 0; col < tableModel.getColumnCount(); col++) {
Object obj = tableModel.getValueAt(row, col);
String val = (obj == null) ? "" : obj.toString();
plainContent.append(val).append('\t');
htmlContent.append(" <td>").append(val).append("</td>\n");
}
plainContent.deleteCharAt(plainContent.length() - 1).append("\n");
htmlContent.append("</tr>\n");
}
plainContent.deleteCharAt(plainContent.length() - 1);
htmlContent.append("</table>\n</body>\n</html>");
Transferable transferable = new BasicTransferable(plainContent.toString(), htmlContent.toString());
try {
Toolkit.getDefaultToolkit().getSystemClipboard().setContents(transferable, null);
} catch (IllegalStateException e) {
View.getSingleton().showWarningDialog(
ScanProgressDialog.this,
Constant.messages.getString("ascan.progress.copyclipboard.error"));
log.warn("Failed to copy the contents to clipboard:", e);
}
}
});
}
return copyToClipboardButton;
}
/**
* Get the main content panel of the dialog
* @return the main panel
*/
private JTable getMainPanel() {
if (table == null) {
model = new ScanProgressTableModel();
table = new JTable();
table.setModel(model);
table.setRowSelectionAllowed(false);
table.setColumnSelectionAllowed(false);
table.setDoubleBuffered(true);
// First column is for plugin's name
table.getColumnModel().getColumn(0).setPreferredWidth(256);
table.getColumnModel().getColumn(1).setPreferredWidth(80);
// Second column is for plugin's status
table.getColumnModel().getColumn(2).setPreferredWidth(80);
table.getColumnModel().getColumn(2).setCellRenderer(new ScanProgressBarRenderer());
// Third column is for plugin's elapsed time
DefaultTableCellRenderer centerRenderer = new DefaultTableCellRenderer();
centerRenderer.setHorizontalAlignment(JLabel.CENTER);
table.getColumnModel().getColumn(3).setPreferredWidth(85);
table.getColumnModel().getColumn(3).setCellRenderer(centerRenderer);
// Forth column is for plugin's request count
DefaultTableCellRenderer rightRenderer = new DefaultTableCellRenderer();
rightRenderer.setHorizontalAlignment(JLabel.RIGHT);
table.getColumnModel().getColumn(4).setPreferredWidth(60);
table.getColumnModel().getColumn(4).setCellRenderer(rightRenderer);
// Fifth column is for plugin's completion and actions
table.getColumnModel().getColumn(5).setPreferredWidth(40);
table.getColumnModel().getColumn(5).setCellRenderer(new ScanProgressActionRenderer());
ScanProgressActionListener listener = new ScanProgressActionListener(table, model);
table.addMouseListener(listener);
table.addMouseMotionListener(listener);
}
return table;
}
/**
* Updates the scan progress shown by the dialogue (scanners' progress/state and chart).
*/
private void updateProgress() {
// Start panel data settings
HostProcess hp = getSelectedHostProcess();
if (scan.getHostProcesses() != null && hp != null) {
// Update the main table entries
model.updateValues(scan, hp);
if (scan.isStopped()) {
this.stopThread = true;
}
if (chart != null) {
ResponseCountSnapshot snapshot = scan.getRequestHistory();
while (snapshot != null) {
try {
Second second = new Second(snapshot.getDate());
this.seriesTotal.add(second, snapshot.getTotal());
this.series100.add(second, snapshot.getResp100());
this.series200.add(second, snapshot.getResp200());
this.series300.add(second, snapshot.getResp300());
this.series400.add(second, snapshot.getResp400());
this.series500.add(second, snapshot.getResp500());
snapshot = scan.getRequestHistory();
for (Plugin plugin : scan.getHostProcesses().get(0).getRunning()) {
if (!labelsAdded.contains(plugin.getName())) {
// Add a vertical line with the plugin name
ValueMarker vm = new ValueMarker(plugin.getTimeStarted().getTime());
double center = chart.getXYPlot().getRangeAxis().getRange().getCentralValue();
if (lastCentre != center) {
if (lastCentre != -1) {
// Move the existing labels so they stay in the centre
@SuppressWarnings("rawtypes")
List annotations = chart.getXYPlot().getAnnotations();
for (Object o: annotations) {
if (o instanceof XYTextAnnotation) {
XYTextAnnotation annotation = (XYTextAnnotation)o;
annotation.setY(center);
}
}
}
lastCentre = center;
}
XYTextAnnotation updateLabel =
new XYTextAnnotation(plugin.getName(),
plugin.getTimeStarted().getTime(), center);
updateLabel.setFont(FontUtils.getFont("Sans Serif"));
updateLabel.setRotationAnchor(TextAnchor.BASELINE_CENTER);
updateLabel.setTextAnchor(TextAnchor.BASELINE_CENTER);
updateLabel.setRotationAngle(-3.14 / 2);
updateLabel.setPaint(Color.black);
chart.getXYPlot().addDomainMarker(vm, Layer.BACKGROUND);
chart.getXYPlot().addAnnotation(updateLabel);
labelsAdded.add(plugin.getName());
}
}
} catch (Exception e) {
log.error(e.getMessage(), e);
snapshot = null;
}
}
}
}
}
private HostProcess getSelectedHostProcess() {
String str = (String)this.getHostSelect().getSelectedItem();
if (str == null) {
return null;
}
for (HostProcess hp : scan.getHostProcesses()) {
if (str.equals(hp.getHostAndPort())) {
return hp;
}
}
return null;
}
/**
* Set the scan that will be shown in this dialog.
*
* @param scan the active scan, might be {@code null}.
*/
public void setActiveScan(ActiveScan scan) {
this.scan = scan;
if (scan == null) {
return;
}
getHostSelect().removeAll();
for (HostProcess hp : scan.getHostProcesses()) {
getHostSelect().addItem(hp.getHostAndPort());
}
Thread thread = new Thread() {
@Override
public void run() {
while (!stopThread) {
SwingUtilities.invokeLater(new Runnable(){
@Override
public void run() {
updateProgress();
}});
try {
sleep(200);
} catch (InterruptedException e) {
// Ignore
}
}
}
};
thread.start();
}
private JComboBox<String> getHostSelect() {
if (hostSelect == null) {
hostSelect = new JComboBox<String>();
hostSelect.addActionListener(new ActionListener(){
@Override
public void actionPerformed(ActionEvent e) {
// Switch results, esp necessary when the scan has finished
updateProgress();
}});
}
return hostSelect;
}
/**
* Custom Renderer for the progress bar plugin column
*/
private class ScanProgressBarRenderer implements TableCellRenderer {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
JComponent result;
if (value != null) {
ScanProgressItem item = (ScanProgressItem)value;
JProgressBar bar = new JProgressBar();
bar.setMaximum(100);
bar.setValue(item.getProgressPercentage());
result = bar;
} else {
result = (JComponent)Box.createGlue();
}
// Set all general configurations
result.setOpaque(true);
result.setBackground(JTABLE_ALTERNATE_BACKGROUND);
return result;
}
}
/**
* Custom Renderer for the actions column (skipping)
*/
private class ScanProgressActionRenderer implements TableCellRenderer {
@Override
public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
JComponent result;
if (value != null) {
ScanProgressActionIcon action = (ScanProgressActionIcon)value;
if (action == model.getFocusedAction()) {
action.setOver();
} else {
action.setNormal();
}
result = action;
} else {
result = (JComponent)Box.createGlue();
}
// Set all general configurations
result.setOpaque(true);
result.setBackground(JTABLE_ALTERNATE_BACKGROUND);
return result;
}
}
/**
* Listener for all Action's management (skipping for now)
*/
private static class ScanProgressActionListener extends MouseAdapter {
/**
* Constant that indicates that a column or row was not found.
*/
private static final int NOT_FOUND = -1;
private final JTable table;
private final ScanProgressTableModel model;
public ScanProgressActionListener(JTable table, ScanProgressTableModel model) {
this.table = table;
this.model = model;
}
@Override
public void mouseClicked(MouseEvent e) {
ScanProgressActionIcon action = getScanProgressAction(e.getPoint());
if (action != null) {
action.invokeAction();
}
}
@Override
public void mousePressed(MouseEvent e) {
ScanProgressActionIcon action = getScanProgressAction(e.getPoint());
if (action != null) {
action.setPressed();
action.repaint();
}
}
@Override
public void mouseReleased(MouseEvent e) {
ScanProgressActionIcon action = getScanProgressAction(e.getPoint());
if (action != null) {
action.setReleased();
action.repaint();
}
}
@Override
public void mouseMoved(MouseEvent me) {
ScanProgressActionIcon action = getScanProgressAction(me.getPoint());
if (action != null) {
model.setFocusedAction(action);
action.repaint();
} else if (model.getFocusedAction() != null) {
model.setFocusedAction(action);
table.repaint();
}
}
/**
* Gets the {@code ScanProgressActionIcon} at the given point, if any.
*
* @param point the point to get the scan progress action icon
* @return the {@code ScanProgressActionIcon} at the given point, or {@code null} if none
*/
private ScanProgressActionIcon getScanProgressAction(Point point) {
int column = table.columnAtPoint(point);
if (column == NOT_FOUND) {
return null;
}
int row = table.rowAtPoint(point);
if (row == NOT_FOUND) {
return null;
}
Object value = table.getValueAt(row, column);
if (value instanceof ScanProgressActionIcon) {
return (ScanProgressActionIcon)value;
}
return null;
}
}
}