diff --git a/.travis.yml b/.travis.yml index 56c8b7bbbd..575fa3f9dc 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,11 @@ language: java sudo: required -dist: bionic -os: - - linux + +jobs: + include: + - os: linux + dist: bionic + - os: osx env: global: @@ -12,6 +15,7 @@ addons: apt: update: true packages: + - testdisk - libafflib-dev - libewf-dev - libpq-dev @@ -29,11 +33,13 @@ addons: update: true packages: - ant - - ant-optional + - wget + - libpq - libewf - gettext - cppunit - afflib + - testdisk python: - "2.7" @@ -43,9 +49,7 @@ before_install: - python setupSleuthkitBranch.py install: - - sudo apt-get install testdisk - - cd sleuthkit/sleuthkit - - ./travis_install_libs.sh + - pushd sleuthkit/sleuthkit && ./travis_install_libs.sh && popd before_script: - if [ $TRAVIS_OS_NAME = linux ]; then @@ -54,13 +58,13 @@ before_script: export PATH=/usr/bin:$PATH; unset JAVA_HOME; fi + - if [ $TRAVIS_OS_NAME = osx ]; then + brew uninstall java --force; + brew cask uninstall java --force; + brew tap homebrew/cask-versions; + brew cask install corretto8; + export JAVA_HOME=/Library/Java/JavaVirtualMachines/amazon-corretto-8.jdk/Contents/Home; + fi + - java -version -script: - - set -e - - echo "Building TSK..." - - ./bootstrap && ./configure --prefix=/usr && make - - pushd bindings/java/ && ant -q dist-PostgreSQL && popd - - echo "Building Autopsy..." && echo -en 'travis_fold:start:script.build\\r' - - cd $TRAVIS_BUILD_DIR/ - - ant build - - echo -en 'travis_fold:end:script.build\\r' +script: ./travis_build.sh diff --git a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerArtifact.java b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerArtifact.java index 39d3203b13..8c8e722bce 100644 --- a/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerArtifact.java +++ b/Core/src/org/sleuthkit/autopsy/corecomponents/DataContentViewerArtifact.java @@ -56,6 +56,10 @@ import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TskCoreException; import org.sleuthkit.datamodel.TskException; import org.netbeans.swing.etable.ETable; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; /** * Instances of this class display the BlackboardArtifacts associated with the @@ -552,6 +556,16 @@ public class DataContentViewerArtifact extends javax.swing.JPanel implements Dat value = dateFormatter.format(new java.util.Date(epoch * 1000)); } break; + case JSON: + // @TODO: 5726 - return a multilevel bulleted list instead of prettyprint JSON + String jsonVal = attr.getValueString(); + + JsonParser parser = new JsonParser(); + JsonObject json = parser.parse(jsonVal).getAsJsonObject(); + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + + value = gson.toJson(json); + break; } /* * Attribute sources column. diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/ArtifactStringContent.java b/Core/src/org/sleuthkit/autopsy/datamodel/ArtifactStringContent.java index 774d1f2f9e..e2950e5b12 100644 --- a/Core/src/org/sleuthkit/autopsy/datamodel/ArtifactStringContent.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/ArtifactStringContent.java @@ -122,6 +122,7 @@ public class ArtifactStringContent implements StringContent { case LONG: case DOUBLE: case BYTE: + case JSON: default: value = attr.getDisplayString(); break; diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java index 66214dd204..871cf9cf7b 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/MapWaypoint.java @@ -38,12 +38,10 @@ import org.sleuthkit.autopsy.directorytree.ExternalViewerShortcutAction; import org.sleuthkit.autopsy.geolocation.datamodel.GeoLocationDataException; import org.sleuthkit.autopsy.geolocation.datamodel.Route; import org.sleuthkit.datamodel.SleuthkitCase; -import org.sleuthkit.autopsy.geolocation.datamodel.ArtifactWaypoint; +import org.sleuthkit.autopsy.geolocation.datamodel.Waypoint; import org.sleuthkit.autopsy.timeline.actions.ViewArtifactInTimelineAction; -import org.sleuthkit.autopsy.timeline.actions.ViewFileInTimelineAction; import org.sleuthkit.datamodel.AbstractFile; import org.sleuthkit.datamodel.BlackboardArtifact; -import org.sleuthkit.datamodel.BlackboardAttribute; import org.sleuthkit.datamodel.Content; import org.sleuthkit.datamodel.TskCoreException; @@ -56,7 +54,7 @@ final class MapWaypoint extends KdTree.XYZPoint implements org.jxmapviewer.viewe private static final Logger logger = Logger.getLogger(MapWaypoint.class.getName()); - private final ArtifactWaypoint dataModelWaypoint; + private final Waypoint dataModelWaypoint; private final GeoPosition position; /** @@ -64,7 +62,7 @@ final class MapWaypoint extends KdTree.XYZPoint implements org.jxmapviewer.viewe * * @param dataModelWaypoint The datamodel waypoint to wrap */ - private MapWaypoint(ArtifactWaypoint dataModelWaypoint) { + private MapWaypoint(Waypoint dataModelWaypoint) { super(dataModelWaypoint.getLatitude(), dataModelWaypoint.getLongitude()); this.dataModelWaypoint = dataModelWaypoint; position = new GeoPosition(dataModelWaypoint.getLatitude(), dataModelWaypoint.getLongitude()); @@ -86,7 +84,7 @@ final class MapWaypoint extends KdTree.XYZPoint implements org.jxmapviewer.viewe * @throws GeoLocationDataException */ static List getWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { - List points = ArtifactWaypoint.getAllWaypoints(skCase); + List points = Waypoint.getAllWaypoints(skCase); List routes = Route.getRoutes(skCase); for (Route route : routes) { @@ -95,7 +93,7 @@ final class MapWaypoint extends KdTree.XYZPoint implements org.jxmapviewer.viewe List mapPoints = new ArrayList<>(); - for (ArtifactWaypoint point : points) { + for (Waypoint point : points) { mapPoints.add(new MapWaypoint(point)); } diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/EXIFWaypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/EXIFWaypoint.java index dc492a5600..3a1ec3e66e 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/EXIFWaypoint.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/EXIFWaypoint.java @@ -27,7 +27,7 @@ import org.sleuthkit.datamodel.TskCoreException; /** * Waypoint wrapper class for TSK_METADATA_EXIF artifacts. */ -final class EXIFWaypoint extends ArtifactWaypoint { +final class EXIFWaypoint extends Waypoint { /** * Construct a way point with the given artifact. diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/LastKnownWaypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/LastKnownWaypoint.java index 8719d043cb..7bf85874ff 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/LastKnownWaypoint.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/LastKnownWaypoint.java @@ -26,7 +26,7 @@ import org.sleuthkit.datamodel.BlackboardAttribute; /** * A Last Known Location Waypoint object. */ -final class LastKnownWaypoint extends ArtifactWaypoint { +final class LastKnownWaypoint extends Waypoint { /** * Constructs a new waypoint. diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Route.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Route.java index 233fa1c8eb..7c608d6f08 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Route.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Route.java @@ -36,12 +36,12 @@ import org.sleuthkit.datamodel.TskCoreException; * */ public final class Route { - private final List points; + private final List points; private final Long timestamp; // This list is not expected to change after construction so the // constructor will take care of creating an unmodifiable List - private final List immutablePropertiesList; + private final List immutablePropertiesList; /** * Gets the list of Routes from the TSK_GPS_ROUTE artifacts. @@ -77,14 +77,14 @@ public final class Route { Route(BlackboardArtifact artifact) throws GeoLocationDataException { points = new ArrayList<>(); - Map attributeMap = ArtifactWaypoint.getAttributesFromArtifactAsMap(artifact); + Map attributeMap = Waypoint.getAttributesFromArtifactAsMap(artifact); points.add(getRouteStartPoint(artifact, attributeMap)); points.add(getRouteEndPoint(artifact, attributeMap)); BlackboardAttribute attribute = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME); timestamp = attribute != null ? attribute.getValueLong() : null; - immutablePropertiesList = Collections.unmodifiableList(ArtifactWaypoint.createGeolocationProperties(attributeMap)); + immutablePropertiesList = Collections.unmodifiableList(Waypoint.createGeolocationProperties(attributeMap)); } /** @@ -92,7 +92,7 @@ public final class Route { * * @return List an unmodifiableList of ArtifactWaypoints for this route */ - public List getRoute() { + public List getRoute() { return Collections.unmodifiableList(points); } @@ -102,7 +102,7 @@ public final class Route { * * @return Map of key, value pairs. */ - public List getOtherProperties() { + public List getOtherProperties() { return immutablePropertiesList; } @@ -136,14 +136,15 @@ public final class Route { @Messages({ "Route_Start_Label=Start" }) - private ArtifactWaypoint getRouteStartPoint(BlackboardArtifact artifact, Map attributeMap) throws GeoLocationDataException { + + private Waypoint getRouteStartPoint(BlackboardArtifact artifact, Map attributeMap) throws GeoLocationDataException { BlackboardAttribute latitude = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_START); BlackboardAttribute longitude = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_START); BlackboardAttribute altitude = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE); BlackboardAttribute pointTimestamp = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME); if (latitude != null && longitude != null) { - return new ArtifactWaypoint(artifact, + return new Waypoint(artifact, Bundle.Route_Start_Label(), pointTimestamp != null ? pointTimestamp.getValueLong() : null, latitude.getValueDouble(), @@ -169,14 +170,15 @@ public final class Route { @Messages({ "Route_End_Label=End" }) - private ArtifactWaypoint getRouteEndPoint(BlackboardArtifact artifact, Map attributeMap) throws GeoLocationDataException { + Waypoint getRouteEndPoint(BlackboardArtifact artifact, Map attributeMap) throws GeoLocationDataException { BlackboardAttribute latitude = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END); BlackboardAttribute longitude = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_END); BlackboardAttribute altitude = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_ALTITUDE); BlackboardAttribute pointTimestamp = attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME); if (latitude != null && longitude != null) { - return new ArtifactWaypoint(artifact, + + return new Waypoint(artifact, Bundle.Route_End_Label(), pointTimestamp != null ? pointTimestamp.getValueLong() : null, latitude.getValueDouble(), diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/SearchWaypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/SearchWaypoint.java index b1757ae58f..7f0746d6ca 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/SearchWaypoint.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/SearchWaypoint.java @@ -24,9 +24,9 @@ import org.sleuthkit.datamodel.BlackboardArtifact; import org.sleuthkit.datamodel.BlackboardAttribute; /** - * A SearchWaypoint is a subclass of ArtifactWaypoint. + * A SearchWaypoint is a subclass of Waypoint. */ -final class SearchWaypoint extends ArtifactWaypoint { +final class SearchWaypoint extends Waypoint { /** * Construct a waypoint for TSK_GPS_SEARCH artifact. diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/TrackpointWaypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/TrackpointWaypoint.java index 2745a15380..7009aa63a1 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/TrackpointWaypoint.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/TrackpointWaypoint.java @@ -26,7 +26,7 @@ import org.sleuthkit.datamodel.BlackboardAttribute; /** * A wrapper class for TSK_GPS_TRACKPOINT artifacts. */ -final class TrackpointWaypoint extends ArtifactWaypoint { +final class TrackpointWaypoint extends Waypoint { /** * Construct a waypoint for trackpoints. * diff --git a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/ArtifactWaypoint.java b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java similarity index 86% rename from Core/src/org/sleuthkit/autopsy/geolocation/datamodel/ArtifactWaypoint.java rename to Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java index 1f70863b3a..bd5b39b6b7 100755 --- a/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/ArtifactWaypoint.java +++ b/Core/src/org/sleuthkit/autopsy/geolocation/datamodel/Waypoint.java @@ -37,7 +37,7 @@ import org.sleuthkit.datamodel.TskCoreException; * Representation of a Waypoint created from a BlackboardArtifact. * */ -public class ArtifactWaypoint { +public class Waypoint { final private Long timestamp; final private Double longitude; @@ -50,13 +50,13 @@ public class ArtifactWaypoint { // This list is not expected to change after construction. The // constructor will take care of making an unmodifiable List - final private List immutablePropertiesList; + final private List immutablePropertiesList; /** * This is a list of attributes that are already being handled by the * by getter functions. */ - static BlackboardAttribute.ATTRIBUTE_TYPE[] ALREADY_HANDLED_ATTRIBUTES = { + static private BlackboardAttribute.ATTRIBUTE_TYPE[] ALREADY_HANDLED_ATTRIBUTES = { BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE, @@ -68,7 +68,7 @@ public class ArtifactWaypoint { BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LATITUDE_END, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_GEO_LONGITUDE_END,}; - private static final Logger logger = Logger.getLogger(ArtifactWaypoint.class.getName()); + private static final Logger logger = Logger.getLogger(Waypoint.class.getName()); /** * Construct a waypoint with the given artifact. @@ -78,7 +78,7 @@ public class ArtifactWaypoint { * @throws GeoLocationDataException Exception will be thrown if artifact did * not have a valid longitude and latitude. */ - ArtifactWaypoint(BlackboardArtifact artifact) throws GeoLocationDataException { + Waypoint(BlackboardArtifact artifact) throws GeoLocationDataException { this(artifact, getAttributesFromArtifactAsMap(artifact)); } @@ -98,7 +98,7 @@ public class ArtifactWaypoint { * @throws GeoLocationDataException Exception will be thrown if artifact did * not have a valid longitude and latitude. */ - ArtifactWaypoint(BlackboardArtifact artifact, String label, Long timestamp, Double latitude, Double longitude, Double altitude, AbstractFile image, Map attributeMap, Route route) throws GeoLocationDataException { + Waypoint(BlackboardArtifact artifact, String label, Long timestamp, Double latitude, Double longitude, Double altitude, AbstractFile image, Map attributeMap, Route route) throws GeoLocationDataException { if (longitude == null || latitude == null) { throw new GeoLocationDataException("Invalid waypoint, null value passed for longitude or latitude"); } @@ -124,7 +124,7 @@ public class ArtifactWaypoint { * * @throws GeoLocationDataException */ - private ArtifactWaypoint(BlackboardArtifact artifact, Map attributeMap) throws GeoLocationDataException { + private Waypoint(BlackboardArtifact artifact, Map attributeMap) throws GeoLocationDataException { this(artifact, getLabelFromArtifact(attributeMap), attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME) != null ? attributeMap.get(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME).getValueLong() : null, @@ -207,7 +207,7 @@ public class ArtifactWaypoint { * * @return A List of waypoint properties */ - public List getOtherProperties() { + public List getOtherProperties() { return immutablePropertiesList; } @@ -276,8 +276,8 @@ public class ArtifactWaypoint { * * @throws GeoLocationDataException */ - public static List getAllWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { - List points = new ArrayList<>(); + public static List getAllWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + List points = new ArrayList<>(); points.addAll(getTrackpointWaypoints(skCase)); points.addAll(getEXIFWaypoints(skCase)); @@ -297,7 +297,7 @@ public class ArtifactWaypoint { * * @throws GeoLocationDataException */ - public static List getTrackpointWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + public static List getTrackpointWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { List artifacts = null; try { artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_TRACKPOINT); @@ -305,10 +305,10 @@ public class ArtifactWaypoint { throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_TRACKPOINT", ex); } - List points = new ArrayList<>(); + List points = new ArrayList<>(); for (BlackboardArtifact artifact : artifacts) { try { - ArtifactWaypoint point = new TrackpointWaypoint(artifact); + Waypoint point = new TrackpointWaypoint(artifact); points.add(point); } catch (GeoLocationDataException ex) { logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_TRACKPOINT artifactID: %d", artifact.getArtifactID())); @@ -326,7 +326,7 @@ public class ArtifactWaypoint { * * @throws GeoLocationDataException */ - static public List getEXIFWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + static public List getEXIFWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { List artifacts = null; try { artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_METADATA_EXIF); @@ -334,11 +334,11 @@ public class ArtifactWaypoint { throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_LAST_KNOWN_LOCATION", ex); } - List points = new ArrayList<>(); + List points = new ArrayList<>(); if (artifacts != null) { for (BlackboardArtifact artifact : artifacts) { try { - ArtifactWaypoint point = new EXIFWaypoint(artifact); + Waypoint point = new EXIFWaypoint(artifact); points.add(point); } catch (GeoLocationDataException ex) { // I am a little relucant to log this error because I suspect @@ -359,7 +359,7 @@ public class ArtifactWaypoint { * * @throws GeoLocationDataException */ - public static List getSearchWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + public static List getSearchWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { List artifacts = null; try { artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_SEARCH); @@ -367,11 +367,11 @@ public class ArtifactWaypoint { throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_SEARCH", ex); } - List points = new ArrayList<>(); + List points = new ArrayList<>(); if (artifacts != null) { for (BlackboardArtifact artifact : artifacts) { try { - ArtifactWaypoint point = new SearchWaypoint(artifact); + Waypoint point = new SearchWaypoint(artifact); points.add(point); } catch (GeoLocationDataException ex) { logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_SEARCH artifactID: %d", artifact.getArtifactID())); @@ -390,7 +390,7 @@ public class ArtifactWaypoint { * * @throws GeoLocationDataException */ - public static List getLastKnownWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + public static List getLastKnownWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { List artifacts = null; try { artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_LAST_KNOWN_LOCATION); @@ -398,11 +398,11 @@ public class ArtifactWaypoint { throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_LAST_KNOWN_LOCATION", ex); } - List points = new ArrayList<>(); + List points = new ArrayList<>(); if (artifacts != null) { for (BlackboardArtifact artifact : artifacts) { try { - ArtifactWaypoint point = new LastKnownWaypoint(artifact); + Waypoint point = new LastKnownWaypoint(artifact); points.add(point); } catch (GeoLocationDataException ex) { logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_LAST_KNOWN_LOCATION artifactID: %d", artifact.getArtifactID())); @@ -421,7 +421,7 @@ public class ArtifactWaypoint { * * @throws GeoLocationDataException */ - public static List getBookmarkWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { + public static List getBookmarkWaypoints(SleuthkitCase skCase) throws GeoLocationDataException { List artifacts = null; try { artifacts = skCase.getBlackboardArtifacts(BlackboardArtifact.ARTIFACT_TYPE.TSK_GPS_BOOKMARK); @@ -429,11 +429,11 @@ public class ArtifactWaypoint { throw new GeoLocationDataException("Unable to get artifacts for type: TSK_GPS_BOOKMARK", ex); } - List points = new ArrayList<>(); + List points = new ArrayList<>(); if (artifacts != null) { for (BlackboardArtifact artifact : artifacts) { try { - ArtifactWaypoint point = new ArtifactWaypoint(artifact); + Waypoint point = new Waypoint(artifact); points.add(point); } catch (GeoLocationDataException ex) { logger.log(Level.WARNING, String.format("No longitude or latitude available for TSK_GPS_BOOKMARK artifactID: %d", artifact.getArtifactID())); @@ -454,8 +454,8 @@ public class ArtifactWaypoint { * * @throws GeoLocationDataException */ - static public List createGeolocationProperties(Map attributeMap) throws GeoLocationDataException { - List list = new ArrayList<>(); + static public List createGeolocationProperties(Map attributeMap) throws GeoLocationDataException { + List list = new ArrayList<>(); Set keys = new HashSet<>(attributeMap.keySet()); @@ -467,7 +467,7 @@ public class ArtifactWaypoint { String key = type.getDisplayName(); String value = attributeMap.get(type).getDisplayString(); - list.add(new ArtifactWaypoint.Property(key, value)); + list.add(new Waypoint.Property(key, value)); } return list; } diff --git a/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java b/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java index c03dfdf94a..8588ba35b8 100644 --- a/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java +++ b/Core/src/org/sleuthkit/autopsy/report/modules/kml/KMLReport.java @@ -730,10 +730,15 @@ class KMLReport implements GeneralReportModule { * * @param route * - * @return + * @return A HTML formatted list of the Route attributes */ +<<<<<<< HEAD private String getFormattedDetails(Route route) { List points = route.getRoute(); +======= + private String getFormattedDetails(Route route) { + List points = route.getRoute(); +>>>>>>> geolocation-datamodel-refactoring StringBuilder result = new StringBuilder(); //NON-NLS result.append(String.format("

%s

", Bundle.Route_Details_Header())) @@ -765,8 +770,13 @@ class KMLReport implements GeneralReportModule { } } +<<<<<<< HEAD List list = route.getOtherProperties(); for(ArtifactWaypoint.Property prop: list) { +======= + List list = route.getOtherProperties(); + for(Waypoint.Property prop: list) { +>>>>>>> geolocation-datamodel-refactoring String value = prop.getValue(); if(value != null && !value.isEmpty()) { result.append(formatAttribute(prop.getDisplayName(), value)); diff --git a/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java b/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java index de3350ad88..38e53b5eb6 100644 --- a/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java +++ b/Core/src/org/sleuthkit/autopsy/report/modules/portablecase/PortableCaseReportModule.java @@ -768,6 +768,7 @@ public class PortableCaseReportModule implements ReportModule { oldAttr.getValueLong())); break; case STRING: + case JSON: newAttrs.add(new BlackboardAttribute(newAttributeType, String.join(",", oldAttr.getSources()), oldAttr.getValueString())); break; diff --git a/Core/src/org/sleuthkit/autopsy/test/CustomArtifactType.java b/Core/src/org/sleuthkit/autopsy/test/CustomArtifactType.java index b27fa29dd6..e584c6bdfc 100644 --- a/Core/src/org/sleuthkit/autopsy/test/CustomArtifactType.java +++ b/Core/src/org/sleuthkit/autopsy/test/CustomArtifactType.java @@ -50,6 +50,8 @@ final class CustomArtifactType { private static final String BYTES_ATTR_DISPLAY_NAME = "Custom Bytes"; private static final String STRING_ATTR_TYPE_NAME = "CUSTOM_STRING_ATTRIBUTE"; private static final String STRING_ATTR_DISPLAY_NAME = "Custom String"; + private static final String JSON_ATTR_TYPE_NAME = "CUSTOM_JSON_ATTRIBUTE"; + private static final String JSON_ATTR_DISPLAY_NAME = "Custom Json"; private static BlackboardArtifact.Type artifactType; private static BlackboardAttribute.Type intAttrType; private static BlackboardAttribute.Type doubleAttrType; @@ -57,6 +59,7 @@ final class CustomArtifactType { private static BlackboardAttribute.Type dateTimeAttrType; private static BlackboardAttribute.Type bytesAttrType; private static BlackboardAttribute.Type stringAttrType; + private static BlackboardAttribute.Type jsonAttrType; /** * Adds the custom artifact type, with its associated custom attribute @@ -73,6 +76,7 @@ final class CustomArtifactType { dateTimeAttrType = blackboard.getOrAddAttributeType(DATETIME_ATTR_TYPE_NAME, BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME, DATETIME_ATTR_DISPLAY_NAME); bytesAttrType = blackboard.getOrAddAttributeType(BYTES_ATTR_TYPE_NAME, BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE, BYTES_ATTR_DISPLAY_NAME); stringAttrType = blackboard.getOrAddAttributeType(STRING_ATTR_TYPE_NAME, BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING, STRING_ATTR_DISPLAY_NAME); + jsonAttrType = blackboard.getOrAddAttributeType(JSON_ATTR_TYPE_NAME, BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON, JSON_ATTR_DISPLAY_NAME); } /** @@ -93,6 +97,7 @@ final class CustomArtifactType { attributes.add(new BlackboardAttribute(dateTimeAttrType, MODULE_NAME, 60L)); attributes.add(new BlackboardAttribute(bytesAttrType, MODULE_NAME, DatatypeConverter.parseHexBinary("ABCD"))); attributes.add(new BlackboardAttribute(stringAttrType, MODULE_NAME, "Zero")); + attributes.add(new BlackboardAttribute(jsonAttrType, MODULE_NAME, "{\"fruit\": \"Apple\",\"size\": \"Large\",\"color\": \"Red\"}")); artifact.addAttributes(attributes); /* diff --git a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExporterSettingsPanel.java b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExporterSettingsPanel.java index 517c1e46a6..001ad7271d 100644 --- a/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExporterSettingsPanel.java +++ b/Experimental/src/org/sleuthkit/autopsy/experimental/autoingest/FileExporterSettingsPanel.java @@ -244,6 +244,8 @@ public final class FileExporterSettingsPanel extends JPanel { comboBoxValueType.addItem(BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING.getLabel()); comboBoxValueType.addItem(BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.DATETIME.getLabel()); comboBoxValueType.addItem(BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.BYTE.getLabel()); + comboBoxValueType.addItem(BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.JSON.getLabel()); + comboBoxValueType.addItem(UNSET); load(); diff --git a/InternalPythonModules/android/fbmessenger.py b/InternalPythonModules/android/fbmessenger.py index 3a678f16d0..2347144f09 100644 --- a/InternalPythonModules/android/fbmessenger.py +++ b/InternalPythonModules/android/fbmessenger.py @@ -45,6 +45,7 @@ from org.sleuthkit.datamodel import Account from org.sleuthkit.datamodel.blackboardutils import CommunicationArtifactsHelper from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection +from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CallMediaType import json import traceback @@ -181,15 +182,242 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): contactsDb.close() - - ## Adds a recipient to given list - def addRecipientToList(self, user_key, recipientList): + ## Extracts recipeint id from 'user_key' column and adds recipient to given list, + ## if the recipeint id is not the same as sender id + def addRecipientToList(self, user_key, senderId, recipientList): if user_key is not None: - recipientId = user_key.replace('FACEBOOK:', '') - recipientList.append(recipientId) + recipientId = user_key.replace('FACEBOOK:', '') + if recipientId != senderId: + recipientList.append(recipientId) + + + ## Extracts sender id from the json in 'sender' column. + def getSenderIdFromJson(self, senderJsonStr): + senderId = None; + if senderJsonStr is not None: + sender_dict = json.loads(senderJsonStr) + senderId = sender_dict['user_key'] + senderId = senderId.replace('FACEBOOK:', '') - ## Analyze messages - def analyzeMessages(self, dataSource, fileManager, context): + return senderId + + ## determines communication direction by comparing senderId with selfAccountId + def deduceDirectionFromSenderId(self, senderId): + direction = CommunicationDirection.UNKNOWN + if senderId is not None: + if senderId == self.selfAccountId: + direction = CommunicationDirection.OUTGOING + else: + direction = CommunicationDirection.INCOMING + return direction + + ## Analyzes messages + def analyzeMessages(self, threadsDb, threadsDBHelper): + try: + + ## Messages are found in the messages table. + ## This query filters messages by msg_type to only get actual user created conversation messages (msg_type 0). + ## The participant ids can be found in the thread_participants table. + ## Participant names are found in thread_users table. + ## Joining these tables produces multiple rows per message, one row for each recipient. + ## The result set is processed to collect the multiple recipients for a given message. + sqlString = """ + SELECT msg_id, text, sender, timestamp_ms, msg_type, messages.thread_key as thread_key, + snippet, thread_participants.user_key as user_key, thread_users.name as name + FROM messages + JOIN thread_participants ON messages.thread_key = thread_participants.thread_key + JOIN thread_users ON thread_participants.user_key = thread_users.user_key + WHERE msg_type = 0 + ORDER BY msg_id + """ + + messagesResultSet = threadsDb.runQuery(sqlString) + if messagesResultSet is not None: + oldMsgId = None + + direction = CommunicationDirection.UNKNOWN + fromId = None + recipientIdsList = None + timeStamp = -1 + msgText = "" + threadId = "" + + while messagesResultSet.next(): + msgId = messagesResultSet.getString("msg_id") + + # new msg begins when msgId changes + if msgId != oldMsgId: + # Create message artifact with collected attributes + if oldMsgId is not None: + messageArtifact = threadsDBHelper.addMessage( + self._MESSAGE_TYPE, + direction, + fromId, + recipientIdsList, + timeStamp, + MessageReadStatus.UNKNOWN, + "", # subject + msgText, + threadId) + + oldMsgId = msgId + + # New message - collect all attributes + recipientIdsList = [] + + ## get sender id by parsing JSON in sender column + fromId = self.getSenderIdFromJson(messagesResultSet.getString("sender")) + direction = self.deduceDirectionFromSenderId(fromId) + + # Get recipient and add to list + self.addRecipientToList(messagesResultSet.getString("user_key"), fromId, + recipientIdsList) + + timeStamp = messagesResultSet.getLong("timestamp_ms") / 1000 + + # Get msg text + # Sometimes there may not be an explict msg text, + # but an app generated snippet instead + msgText = messagesResultSet.getString("text") + if not msgText: + msgText = messagesResultSet.getString("snippet") + + # TBD: get attachment + + threadId = messagesResultSet.getString("thread_key") + + else: # same msgId as last, just collect recipient from current row + self.addRecipientToList(messagesResultSet.getString("user_key"), fromId, + recipientIdsList) + + + # at the end of the loop, add last message + messageArtifact = threadsDBHelper.addMessage( + self._MESSAGE_TYPE, + direction, + fromId, + recipientIdsList, + timeStamp, + MessageReadStatus.UNKNOWN, + "", # subject + msgText, + threadId) + + except SQLException as ex: + self._logger.log(Level.WARNING, "Error processing query result for FB Messenger messages.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + self._logger.log(Level.SEVERE, "Failed to add FB Messenger message artifacts.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + self._logger.log(Level.WARNING, "Failed to post artifacts.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + ## Analyzes call logs + def analyzeCallLogs(self, threadsDb, threadsDBHelper): + try: + + ## Call logs are found in the messages table. + ## msg_type indicates type of call: + ## 9: one to one calls + ## 203: group call + ## 1-to-1 calls only have a call_ended record. + ## group calls have a call_started_record as well as call_ended recorded, with *different* message ids. + ## all the data we need can be found in the call_ended record. + + sqlString = """ + SELECT msg_id, text, sender, timestamp_ms, msg_type, admin_text_thread_rtc_event, + generic_admin_message_extensible_data, + messages.thread_key as thread_key, + thread_participants.user_key as user_key, + thread_users.name as name + FROM messages + JOIN thread_participants ON messages.thread_key = thread_participants.thread_key + JOIN thread_users ON thread_participants.user_key = thread_users.user_key + WHERE msg_type = 9 OR (msg_type = 203 AND admin_text_thread_rtc_event = 'group_call_ended') + ORDER BY msg_id + """ + + messagesResultSet = threadsDb.runQuery(sqlString) + if messagesResultSet is not None: + oldMsgId = None + + direction = CommunicationDirection.UNKNOWN + callerId = None + calleeIdsList = None + startTimeStamp = -1 + endTimeStamp = -1 + duration = 0 + mediaType = CallMediaType.AUDIO + + while messagesResultSet.next(): + msgId = messagesResultSet.getString("msg_id") + + # new call begins when msgId changes + if msgId != oldMsgId: + # Create call log artifact with collected attributes + if oldMsgId is not None: + messageArtifact = threadsDBHelper.addCalllog( + direction, + callerId, + calleeIdsList, + startTimeStamp, + endTimeStamp, + mediaType ) + + oldMsgId = msgId + + # New message - collect all attributes + calleeIdsList = [] + + ## get caller id by parsing JSON in sender column + callerId = self.getSenderIdFromJson(messagesResultSet.getString("sender")) + direction = self.deduceDirectionFromSenderId(callerId) + + # Get recipient and add to list + self.addRecipientToList(messagesResultSet.getString("user_key"), callerId, + calleeIdsList) + + # the timestamp from call ended msg is used as end timestamp + endTimeStamp = messagesResultSet.getLong("timestamp_ms") / 1000 + + # parse the generic_admin_message_extensible_data JSON to extract the duration and video fields + adminDataJsonStr = messagesResultSet.getString("generic_admin_message_extensible_data") + if adminDataJsonStr is not None: + adminData_dict = json.loads(adminDataJsonStr) + duration = adminData_dict['call_duration'] # call duration in seconds + isVideo = adminData_dict['video'] + if isVideo: + mediaType = CallMediaType.VIDEO + + startTimeStamp = endTimeStamp - duration + + else: # same msgId as last, just collect callee from current row + self.addRecipientToList(messagesResultSet.getString("user_key"), callerId, + calleeIdsList) + + # at the end of the loop, add last message + messageArtifact = threadsDBHelper.addCalllog( + direction, + callerId, + calleeIdsList, + startTimeStamp, + endTimeStamp, + mediaType ) + + except SQLException as ex: + self._logger.log(Level.WARNING, "Error processing query result for FB Messenger call logs.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + except TskCoreException as ex: + self._logger.log(Level.SEVERE, "Failed to add FB Messenger call log artifacts.", ex) + self._logger.log(Level.SEVERE, traceback.format_exc()) + except BlackboardException as ex: + self._logger.log(Level.WARNING, "Failed to post FB Messenger call log artifacts.", ex) + self._logger.log(Level.WARNING, traceback.format_exc()) + + + ## Analyze messages and call log threads + def analyzeMessagesAndCallLogs(self, dataSource, fileManager, context): threadsDbs = AppSQLiteDB.findAppDatabases(dataSource, "threads_db2", True, self._FB_MESSENGER_PACKAGE_NAME) for threadsDb in threadsDbs: try: @@ -202,113 +430,12 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): self._MODULE_NAME, threadsDb.getDBFile(), Account.Type.FACEBOOK) - ## Messages are found in the messages table. - ## This query filters messages by msg_type to only get actual user created conversation messages (msg_type 0). - ## The participant ids can be found in the thread_participants table. - ## Participant names are found in thread_users table. - ## Joining these tables produces multiple rows per message, one row for each recipient. - ## The result set is processed to collect the multiple recipients for a given message. - sqlString = """ - SELECT msg_id, text, sender, timestamp_ms, msg_type, messages.thread_key as thread_key, - snippet, thread_participants.user_key as user_key, thread_users.name as name - FROM messages - JOIN thread_participants ON messages.thread_key = thread_participants.thread_key - JOIN thread_users ON thread_participants.user_key = thread_users.user_key - WHERE msg_type = 0 - ORDER BY msg_id - """ - - messagesResultSet = threadsDb.runQuery(sqlString) - if messagesResultSet is not None: - oldMsgId = None - - direction = CommunicationDirection.UNKNOWN - fromId = None - recipientIdsList = None - timeStamp = -1 - msgText = "" - threadId = "" - - while messagesResultSet.next(): - msgId = messagesResultSet.getString("msg_id") - - # new msg begins when msgId changes - if msgId != oldMsgId: - # Create message artifact with collected attributes - if oldMsgId is not None: - messageArtifact = threadsDBHelper.addMessage( - self._MESSAGE_TYPE, - direction, - fromId, - recipientIdsList, - timeStamp, - MessageReadStatus.UNKNOWN, - "", # subject - msgText, - threadId) - - oldMsgId = msgId - - # New message - collect all attributes - recipientIdsList = [] - - ## get sender id by parsing JSON in sender column - senderJsonStr = messagesResultSet.getString("sender") - if senderJsonStr is not None: - sender_dict = json.loads(senderJsonStr) - senderId = sender_dict['user_key'] - senderId = senderId.replace('FACEBOOK:', '') - senderName = sender_dict['name'] - fromId = senderId - if senderId == self.selfAccountId: - direction = CommunicationDirection.OUTGOING - else: - direction = CommunicationDirection.INCOMING - - - # Get recipient and add to list - self.addRecipientToList(messagesResultSet.getString("user_key"), - recipientIdsList) - - timeStamp = messagesResultSet.getLong("timestamp_ms") / 1000 - - # Get msg text - # Sometimes there may not be an explict msg text, - # but an app generated snippet instead - msgText = messagesResultSet.getString("text") - if not msgText: - msgText = messagesResultSet.getString("snippet") - - # TBD: get attachment - - threadId = messagesResultSet.getString("thread_key") - - else: # same msgId as last, just collect recipient from current row - self.addRecipientToList(messagesResultSet.getString("user_key"), - recipientIdsList) - - - # at the end of the loop, add last message - messageArtifact = threadsDBHelper.addMessage( - self._MESSAGE_TYPE, - direction, - fromId, - recipientIdsList, - timeStamp, - MessageReadStatus.UNKNOWN, - "", # subject - msgText, - threadId) + self.analyzeMessages(threadsDb, threadsDBHelper) + self.analyzeCallLogs(threadsDb, threadsDBHelper) - except SQLException as ex: - self._logger.log(Level.WARNING, "Error processing query result for FB Messenger messages.", ex) - self._logger.log(Level.WARNING, traceback.format_exc()) except TskCoreException as ex: - self._logger.log(Level.SEVERE, "Failed to add FB Messenger message artifacts.", ex) + self._logger.log(Level.SEVERE, "Failed to to create CommunicationArtifactsHelper for FB Messenger.", ex) self._logger.log(Level.SEVERE, traceback.format_exc()) - except BlackboardException as ex: - self._logger.log(Level.WARNING, "Failed to post artifacts.", ex) - self._logger.log(Level.WARNING, traceback.format_exc()) finally: threadsDb.close() @@ -321,6 +448,6 @@ class FBMessengerAnalyzer(general.AndroidComponentAnalyzer): return self.analyzeContacts(dataSource, fileManager, context) - self.analyzeMessages(dataSource, fileManager, context) + self.analyzeMessagesAndCallLogs(dataSource, fileManager, context) diff --git a/InternalPythonModules/android/imo.py b/InternalPythonModules/android/imo.py index 25dd750c1e..5a1bd89778 100644 --- a/InternalPythonModules/android/imo.py +++ b/InternalPythonModules/android/imo.py @@ -43,10 +43,14 @@ from org.sleuthkit.datamodel import Content from org.sleuthkit.datamodel import TskCoreException from org.sleuthkit.datamodel.Blackboard import BlackboardException from org.sleuthkit.datamodel import Account +from org.sleuthkit.datamodel.blackboardutils import FileAttachment +from org.sleuthkit.datamodel.blackboardutils import URLAttachment +from org.sleuthkit.datamodel.blackboardutils import MessageAttachments from org.sleuthkit.datamodel.blackboardutils import CommunicationArtifactsHelper from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import MessageReadStatus from org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper import CommunicationDirection +import json import traceback import general @@ -66,6 +70,8 @@ class IMOAnalyzer(general.AndroidComponentAnalyzer): -- A messages table which stores the message details --- sender/receiver buid, timestamp, message_type (1: incoming, 0: outgoing), message_read... --- 'imdata' column stores a json structure with all the message details, including attachments + ---- attachment file path may be specified in local_path or original_path. Original path, if available is a better candidate. + ---- For sent files, files seem to get uploaded to IMO Servers. There is no URL available in the imdata though. """ @@ -156,7 +162,7 @@ class IMOAnalyzer(general.AndroidComponentAnalyzer): msgReadStatus = MessageReadStatus.UNKNOWN timeStamp = messagesResultSet.getLong("timestamp") / 1000000000 - + msgBody = messagesResultSet.getString("last_message") messageArtifact = friendsDBHelper.addMessage( self._MESSAGE_TYPE, @@ -166,12 +172,34 @@ class IMOAnalyzer(general.AndroidComponentAnalyzer): timeStamp, msgReadStatus, "", # subject - messagesResultSet.getString("last_message"), + msgBody, "") # thread id - - # TBD: parse the imdata JSON structure to figure out if there is an attachment. - # If one exists, add the attachment as a derived file and a child of the message artifact. + + # Parse the imdata JSON structure to check if there is an attachment. + # If one exists, create an attachment and add to the message. + fileAttachments = ArrayList() + urlAttachments = ArrayList() + + imdataJsonStr = messagesResultSet.getString("imdata") + if imdataJsonStr is not None: + imdata_dict = json.loads(imdataJsonStr) + + # set to none if the key doesn't exist in the dict + attachmentOriginalPath = imdata_dict.get('original_path', None) + attachmentLocalPath = imdata_dict.get('local_path', None) + if attachmentOriginalPath: + attachmentPath = attachmentOriginalPath + else: + attachmentPath = attachmentLocalPath + + if attachmentPath: + # Create a file attachment with given path + fileAttachment = FileAttachment(current_case.getSleuthkitCase(), friendsDb.getDBFile().getDataSource(), attachmentPath) + fileAttachments.add(fileAttachment) + + msgAttachments = MessageAttachments(fileAttachments, []) + attachmentArtifact = friendsDBHelper.addAttachments(messageArtifact, msgAttachments) except SQLException as ex: self._logger.log(Level.WARNING, "Error processing query result for IMO friends", ex) diff --git a/travis_build.sh b/travis_build.sh new file mode 100644 index 0000000000..d25d52d6ae --- /dev/null +++ b/travis_build.sh @@ -0,0 +1,12 @@ +#!/bin/bash + +set -e +echo "Building TSK..." +cd sleuthkit/sleuthkit +./bootstrap && configure --prefix=/usr && make +pushd bindings/java && ant -q dist-PostgreSQL && popd + +echo "Building Autopsy..." && echo -en 'travis_fold:start:script.build\\r' +cd $TRAVIS_BUILD_DIR/ +ant build +echo -en 'travis_fold:end:script.build\\r'