Autopsy  4.12.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
UserMetricGraphPanel.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2018 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this file except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.healthmonitor;
20 
21 import java.awt.Color;
22 import java.awt.FontMetrics;
23 import java.awt.Graphics;
24 import java.awt.Graphics2D;
25 import java.awt.RenderingHints;
26 import java.util.Collections;
27 import java.util.Comparator;
28 import java.util.ArrayList;
29 import java.util.List;
30 import java.util.Map;
31 import java.util.Set;
32 import java.util.HashMap;
33 import java.util.HashSet;
34 import java.util.TreeSet;
35 import java.util.Calendar;
36 import java.util.GregorianCalendar;
37 import javax.swing.JPanel;
38 import java.util.TimeZone;
39 import java.util.concurrent.TimeUnit;
40 import org.openide.util.NbBundle;
42 
46 class UserMetricGraphPanel extends JPanel {
47 
48  private static final int padding = 25;
49  private static final int labelPadding = 25;
50  private final Color examinerColor = new Color(0x12, 0x20, 0xdb, 255);
51  private final Color autoIngestColor = new Color(0x12, 0x80, 0x20, 255);
52  private final Color gridColor = new Color(200, 200, 200, 200);
53  private static final int pointWidth = 4;
54  private static final int numberYDivisions = 10;
55  private final List<UserCount> dataToPlot;
56  private final String graphLabel;
57  private final long dataInterval;
58  private final long MILLISECONDS_PER_HOUR = 1000 * 60 * 60;
59  private final long MILLISECONDS_PER_DAY = MILLISECONDS_PER_HOUR * 24;
60  private final long maxTimestamp;
61  private final long minTimestamp;
62  private int maxCount;
63  private static final int minCount = 0; // The bottom of the graph will always be zero
64 
65  @NbBundle.Messages({"UserMetricGraphPanel.constructor.casesOpen=Cases open",
66  "UserMetricGraphPanel.constructor.loggedIn=Users logged in - examiner nodes in blue, auto ingest nodes in green"
67  })
68  UserMetricGraphPanel(List<UserData> userResults, long timestampThreshold, boolean plotCases) {
69 
70  maxTimestamp = System.currentTimeMillis();
71  minTimestamp = timestampThreshold;
72 
73  // Make the label
74  if (plotCases) {
75  graphLabel = Bundle.UserMetricGraphPanel_constructor_casesOpen();
76  } else {
77  graphLabel = Bundle.UserMetricGraphPanel_constructor_loggedIn();
78  }
79 
80  // Comparator for the set of UserData objects
81  Comparator<UserData> sortOnTimestamp = new Comparator<UserData>() {
82  @Override
83  public int compare(UserData o1, UserData o2) {
84  return Long.compare(o1.getTimestamp(), o2.getTimestamp());
85  }
86  };
87 
88  // Create a map from host name to data and get the timestamp bounds.
89  // We're using TreeSets here because they support the floor function.
90  Map<String, TreeSet<UserData>> userDataMap = new HashMap<>();
91  for(UserData result:userResults) {
92  if(userDataMap.containsKey(result.getHostname())) {
93  userDataMap.get(result.getHostname()).add(result);
94  } else {
95  TreeSet<UserData> resultTreeSet = new TreeSet<>(sortOnTimestamp);
96  resultTreeSet.add(result);
97  userDataMap.put(result.getHostname(), resultTreeSet);
98  }
99  }
100 
101  // Create a list of data points to plot
102  // The idea here is that starting at maxTimestamp, we go backwards in increments,
103  // see what the state of each node was at that time and make the counts of nodes
104  // that are logged in/ have a case open.
105  // A case is open if the last event was "case open"; closed otherwise
106  // A user is logged in if the last event was anything but "log out";logged out otherwise
107  dataToPlot = new ArrayList<>();
108  dataInterval = MILLISECONDS_PER_HOUR;
109  maxCount = Integer.MIN_VALUE;
110  for (long timestamp = maxTimestamp;timestamp > minTimestamp;timestamp -= dataInterval) {
111 
112  // Collect both counts so that we can use the same scale in the open case graph and
113  // the logged in users graph
114  UserCount openCaseCount = new UserCount(timestamp);
115  UserCount loggedInUserCount = new UserCount(timestamp);
116 
117  Set<String> openCaseNames = new HashSet<>();
118  UserData timestampUserData = UserData.createDummyUserData(timestamp);
119 
120  for (String hostname:userDataMap.keySet()) {
121  // Get the most recent record before this timestamp
122  UserData lastRecord = userDataMap.get(hostname).floor(timestampUserData);
123 
124  if (lastRecord != null) {
125 
126  // Update the case count.
127  if (lastRecord.getEventType().caseIsOpen()) {
128 
129  // Only add each case once regardless of how many users have it open
130  if ( ! openCaseNames.contains(lastRecord.getCaseName())) {
131 
132  // Store everything as examiner nodes. The graph will represent
133  // the number of distinct cases open, not anything about the
134  // nodes that have them open.
135  openCaseCount.addExaminer();
136  openCaseNames.add(lastRecord.getCaseName());
137  }
138  }
139 
140  // Update the logged in user count
141  if (lastRecord.getEventType().userIsLoggedIn()) {
142  if(lastRecord.isExaminerNode()) {
143  loggedInUserCount.addExaminer();
144  } else {
145  loggedInUserCount.addAutoIngestNode();
146  }
147  }
148  }
149  }
150 
151  // Check if this is a new maximum.
152  // Assuming we log all the events, there should never be more cases open than
153  // there are logged in users, but it could happen if we lose data.
154  maxCount = Integer.max(maxCount, openCaseCount.getTotalNodeCount());
155  maxCount = Integer.max(maxCount, loggedInUserCount.getTotalNodeCount());
156 
157  // Add the count to be plotted
158  if(plotCases) {
159  dataToPlot.add(openCaseCount);
160  } else {
161  dataToPlot.add(loggedInUserCount);
162  }
163  }
164  }
165 
177  @Override
178  protected void paintComponent(Graphics g) {
179  super.paintComponent(g);
180  Graphics2D g2 = (Graphics2D) g;
181  g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
182 
183  // Get the max and min timestamps to create the x-axis.
184  // We add a small buffer to each side so the data won't overwrite the axes.
185  double maxValueOnXAxis = maxTimestamp + TimeUnit.HOURS.toMillis(2); // Two hour buffer (the last bar graph will take up one of the hours)
186  double minValueOnXAxis = minTimestamp - TimeUnit.HOURS.toMillis(1); // One hour buffer
187 
188  // Get the max and min times to create the y-axis
189  // To make the intervals even, make sure the maximum is a multiple of five
190  if((maxCount % 5) != 0) {
191  maxCount += (5 - (maxCount % 5));
192  }
193  int maxValueOnYAxis = Integer.max(maxCount, 5);
194  int minValueOnYAxis = minCount;
195 
196  // The graph itself has the following corners:
197  // (padding + label padding, padding + font height) -> top left
198  // (padding + label padding, getHeight() - label padding - padding) -> bottom left
199  // (getWidth() - padding, padding + font height) -> top right
200  // (padding + label padding, getHeight() - label padding - padding) -> bottom right
201  int leftGraphPadding = padding + labelPadding;
202  int rightGraphPadding = padding;
203  int topGraphPadding = padding + g2.getFontMetrics().getHeight();
204  int bottomGraphPadding = labelPadding;
205 
206  // Calculate the scale for each axis.
207  // The size of the graph area is the width/height of the panel minus any padding.
208  // The scale is calculated based on this size of the graph compared to the data range.
209  // For example:
210  // getWidth() = 575 => graph width = 500
211  // If our max x value to plot is 10000 and our min is 0, then the xScale would be 0.05 - i.e.,
212  // our original x values will be multipled by 0.05 to translate them to an x-coordinate in the
213  // graph (plus the padding)
214  int graphWidth = getWidth() - leftGraphPadding - rightGraphPadding;
215  int graphHeight = getHeight() - topGraphPadding - bottomGraphPadding;
216  double xScale = ((double) graphWidth) / (maxValueOnXAxis - minValueOnXAxis);
217  double yScale = ((double) graphHeight) / (maxValueOnYAxis - minValueOnYAxis);
218 
219  // Draw white background
220  g2.setColor(Color.WHITE);
221  g2.fillRect(leftGraphPadding, topGraphPadding, graphWidth, graphHeight);
222 
223  // Create hatch marks and grid lines for y axis.
224  int labelWidth;
225  int positionForMetricNameLabel = 0;
226  Map<Integer, Integer> countToGraphPosition = new HashMap<>();
227  for (int i = 0; i < numberYDivisions + 1; i++) {
228  int x0 = leftGraphPadding;
229  int x1 = pointWidth + leftGraphPadding;
230  int y0 = getHeight() - ((i * graphHeight) / numberYDivisions + bottomGraphPadding);
231  int y1 = y0;
232 
233  if ( ! dataToPlot.isEmpty()) {
234  // Draw the grid line
235  g2.setColor(gridColor);
236  g2.drawLine(leftGraphPadding + 1 + pointWidth, y0, getWidth() - rightGraphPadding, y1);
237 
238  // Create the label
239  g2.setColor(Color.BLACK);
240  double yValue = minValueOnYAxis + ((maxValueOnYAxis - minValueOnYAxis) * ((i * 1.0) / numberYDivisions));
241  int intermediateLabelVal = (int) (yValue * 100);
242  if ((i == numberYDivisions) || ((intermediateLabelVal % 100) == 0)) {
243  countToGraphPosition.put(intermediateLabelVal / 100, y0);
244  String yLabel = Integer.toString(intermediateLabelVal / 100);
245  FontMetrics fontMetrics = g2.getFontMetrics();
246  labelWidth = fontMetrics.stringWidth(yLabel);
247  g2.drawString(yLabel, x0 - labelWidth - 5, y0 + (fontMetrics.getHeight() / 2) - 3);
248 
249  // The nicest looking alignment for this label seems to be left-aligned with the top
250  // y-axis label. Save this position to be used to write the label later.
251  if (i == numberYDivisions) {
252  positionForMetricNameLabel = x0 - labelWidth - 5;
253  }
254  }
255  }
256 
257  // Draw the small hatch mark
258  g2.setColor(Color.BLACK);
259  g2.drawLine(x0, y0, x1, y1);
260  }
261 
262  // On the x-axis, the farthest right grid line should represent midnight preceding the last recorded value
263  Calendar maxDate = new GregorianCalendar();
264  maxDate.setTimeInMillis(maxTimestamp);
265  maxDate.set(Calendar.HOUR_OF_DAY, 0);
266  maxDate.set(Calendar.MINUTE, 0);
267  maxDate.set(Calendar.SECOND, 0);
268  maxDate.set(Calendar.MILLISECOND, 0);
269  long maxMidnightInMillis = maxDate.getTimeInMillis();
270 
271  // We don't want to display more than 20 grid lines. If we have more
272  // data then that, put multiple days within one division
273  long totalDays = (maxMidnightInMillis - (long)minValueOnXAxis) / MILLISECONDS_PER_DAY;
274  long daysPerDivision;
275  if(totalDays <= 20) {
276  daysPerDivision = 1;
277  } else {
278  daysPerDivision = (totalDays / 20);
279  if((totalDays % 20) != 0) {
280  daysPerDivision++;
281  }
282  }
283 
284  // Draw the vertical grid lines and labels
285  // The vertical grid lines will be at midnight, and display the date underneath them
286  // At present we use GMT because of some complications with daylight savings time.
287  for (long currentDivision = maxMidnightInMillis; currentDivision >= minValueOnXAxis; currentDivision -= MILLISECONDS_PER_DAY * daysPerDivision) {
288 
289  int x0 = (int) ((currentDivision - minValueOnXAxis) * xScale + leftGraphPadding);
290  int x1 = x0;
291  int y0 = getHeight() - bottomGraphPadding;
292  int y1 = y0 - pointWidth;
293 
294  // Draw the light grey grid line
295  g2.setColor(gridColor);
296  g2.drawLine(x0, getHeight() - bottomGraphPadding - 1 - pointWidth, x1, topGraphPadding);
297 
298  // Draw the hatch mark
299  g2.setColor(Color.BLACK);
300  g2.drawLine(x0, y0, x1, y1);
301 
302  // Draw the label
303  Calendar thisDate = new GregorianCalendar();
304  thisDate.setTimeZone(TimeZone.getTimeZone("GMT")); // Stick with GMT to avoid daylight savings issues
305  thisDate.setTimeInMillis(currentDivision);
306  int month = thisDate.get(Calendar.MONTH) + 1;
307  int day = thisDate.get(Calendar.DAY_OF_MONTH);
308 
309  String xLabel = month + "/" + day;
310  FontMetrics metrics = g2.getFontMetrics();
311  labelWidth = metrics.stringWidth(xLabel);
312  g2.drawString(xLabel, x0 - labelWidth / 2, y0 + metrics.getHeight() + 3);
313  }
314 
315  // Create x and y axes
316  g2.setColor(Color.BLACK);
317  g2.drawLine(leftGraphPadding, getHeight() - bottomGraphPadding, leftGraphPadding, topGraphPadding);
318  g2.drawLine(leftGraphPadding, getHeight() - bottomGraphPadding, getWidth() - rightGraphPadding, getHeight() - bottomGraphPadding);
319 
320  // Sort dataToPlot on timestamp
321  Collections.sort(dataToPlot, new Comparator<UserCount>(){
322  @Override
323  public int compare(UserCount o1, UserCount o2){
324  return Long.compare(o1.getTimestamp(), o2.getTimestamp());
325  }
326  });
327 
328  // Create the bars
329  for(int i = 0;i < dataToPlot.size();i++) {
330  UserCount userCount = dataToPlot.get(i);
331  int x = (int) ((userCount.getTimestamp() - minValueOnXAxis) * xScale + leftGraphPadding);
332  int yTopOfExaminerBox;
333  if(countToGraphPosition.containsKey(userCount.getTotalNodeCount())) {
334  // If we've drawn a grid line for this count, use the recorded value. If we don't do
335  // this, rounding differences lead to the bar graph not quite lining up with the existing grid.
336  yTopOfExaminerBox = countToGraphPosition.get(userCount.getTotalNodeCount());
337  } else {
338  yTopOfExaminerBox = (int) ((maxValueOnYAxis - userCount.getTotalNodeCount()) * yScale + topGraphPadding);
339  }
340 
341  // Calculate the width. If this isn't the last column, set this to one less than
342  // the distance to the next column starting point.
343  int width;
344  if(i < dataToPlot.size() - 1) {
345  width = Integer.max((int)((dataToPlot.get(i + 1).getTimestamp() - minValueOnXAxis) * xScale + leftGraphPadding) - x - 1,
346  1);
347  } else {
348  width = Integer.max((int)(dataInterval * xScale), 1);
349  }
350 
351  // The examiner bar goes all the way to the bottom of the graph.
352  // The bottom will be overwritten by the auto ingest bar for displaying
353  // logged in users.
354  int heightExaminerBox = (getHeight() - bottomGraphPadding) - yTopOfExaminerBox;
355 
356  // Plot the examiner bar
357  g2.setColor(examinerColor);
358  g2.fillRect(x, yTopOfExaminerBox, width, heightExaminerBox);
359 
360  // Check that there is an auto ingest node count before plotting its bar.
361  // For the cases open graph, this will always be empty.
362  if (userCount.getAutoIngestNodeCount() > 0) {
363  int yTopOfAutoIngestBox;
364  if(countToGraphPosition.containsKey(userCount.getAutoIngestNodeCount())) {
365  // As above, if we've drawn a grid line for this count, use the recorded value. If we don't do
366  // this, rounding differences lead to the bar graph not quite lining up with the existing grid.
367  yTopOfAutoIngestBox =countToGraphPosition.get(userCount.getAutoIngestNodeCount());
368  } else {
369  yTopOfAutoIngestBox = yTopOfExaminerBox + heightExaminerBox;
370  }
371  int heightAutoIngestBox = (getHeight() - bottomGraphPadding) - yTopOfAutoIngestBox;
372 
373  // Plot the auto ingest bar
374  g2.setColor(autoIngestColor);
375  g2.fillRect(x, yTopOfAutoIngestBox, width, heightAutoIngestBox);
376  }
377  }
378 
379  // The graph lines may have extended up past the bounds of the graph. Overwrite that
380  // area with the original background color.
381  g2.setColor(this.getBackground());
382  g2.fillRect(leftGraphPadding, 0, graphWidth, topGraphPadding);
383 
384  // Write the scale. Do this after we erase the top block of the graph.
385  g2.setColor(Color.BLACK);
386  String titleStr = graphLabel;
387  g2.drawString(titleStr, positionForMetricNameLabel, padding);
388  }
389 
394  private class UserCount {
395  private final long timestamp;
396  private int examinerCount;
397  private int autoIngestCount;
398 
403  UserCount(long timestamp) {
404  this.timestamp = timestamp;
405  this.examinerCount = 0;
406  this.autoIngestCount = 0;
407  }
408 
412  void addExaminer() {
413  examinerCount++;
414  }
415 
419  void addAutoIngestNode() {
420  autoIngestCount++;
421  }
422 
427  int getExaminerNodeCount() {
428  return examinerCount;
429  }
430 
435  int getAutoIngestNodeCount() {
436  return autoIngestCount;
437  }
438 
443  int getTotalNodeCount() {
444  return examinerCount + autoIngestCount;
445  }
446 
451  long getTimestamp() {
452  return timestamp;
453  }
454  }
455 }

Copyright © 2012-2018 Basis Technology. Generated on: Wed Sep 18 2019
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.