Autopsy 4.22.1
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 */
19package org.sleuthkit.autopsy.healthmonitor;
20
21import java.awt.Color;
22import java.awt.FontMetrics;
23import java.awt.Graphics;
24import java.awt.Graphics2D;
25import java.awt.RenderingHints;
26import java.util.Collections;
27import java.util.Comparator;
28import java.util.ArrayList;
29import java.util.List;
30import java.util.Map;
31import java.util.Set;
32import java.util.HashMap;
33import java.util.HashSet;
34import java.util.TreeSet;
35import java.util.Calendar;
36import java.util.GregorianCalendar;
37import javax.swing.JPanel;
38import java.util.TimeZone;
39import java.util.concurrent.TimeUnit;
40import org.openide.util.NbBundle;
41import org.sleuthkit.autopsy.healthmonitor.HealthMonitor.UserData;
42
46class 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() {
414 }
415
419 void addAutoIngestNode() {
421 }
422
427 int getExaminerNodeCount() {
428 return examinerCount;
429 }
430
435 int getAutoIngestNodeCount() {
436 return autoIngestCount;
437 }
438
443 int getTotalNodeCount() {
445 }
446
451 long getTimestamp() {
452 return timestamp;
453 }
454 }
455}

Copyright © 2012-2024 Sleuth Kit Labs. Generated on:
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.