def testTSKListDirectory(self): """Test directory listing in sleuthkit.""" path = os.path.join(self.base_path, u"test_img.dd") ps2 = rdf_paths.PathSpec(path=u"入乡随俗 海外春节别样过法", pathtype=rdf_paths.PathSpec.PathType.TSK) ps = rdf_paths.PathSpec(path=path, pathtype=rdf_paths.PathSpec.PathType.OS) ps.Append(ps2) directory = vfs.VFSOpen(ps) self.CheckDirectoryListing(directory, u"入乡随俗.txt")
def testGuessPathSpec(self): """Test that we can guess a pathspec from a path.""" path = os.path.join(self.base_path, "test_img.dd", "home/image2.img", "home/a.txt") pathspec = rdf_paths.PathSpec(path=path, pathtype=rdf_paths.PathSpec.PathType.OS) fd = vfs.VFSOpen(pathspec) self.assertEqual(fd.read(3), "yay")
def Run(self, args): """Run.""" # This action might crash the box so we need to flush the transaction log. self.SyncTransactionLog() if args.pathtype != "MEMORY": raise RuntimeError("Can only GetMemoryInformation on memory devices.") with vfs.VFSOpen(args) as fd: self.SendReply(fd.GetMemoryInformation())
def Run(self, args): """Sends a StatEntry for a single file.""" try: fd = vfs.VFSOpen(args.pathspec, progress_callback=self.Progress) res = fd.Stat() self.SendReply(res) except (IOError, OSError), e: self.SetStatus(rdf_flows.GrrStatus.ReturnedStatus.IOERROR, e) return
def Run(self, args): """Fingerprint a file.""" with vfs.VFSOpen(args.pathspec, progress_callback=self.Progress) as file_obj: fingerprinter = Fingerprinter(self.Progress, file_obj) response = rdf_client.FingerprintResponse() response.pathspec = file_obj.pathspec if args.tuples: tuples = args.tuples else: # There are none selected -- we will cover everything tuples = list() for k in self._fingerprint_types.iterkeys(): tuples.append(rdf_client.FingerprintTuple(fp_type=k)) for finger in tuples: hashers = [self._hash_types[h] for h in finger.hashers] or None if finger.fp_type in self._fingerprint_types: invoke = self._fingerprint_types[finger.fp_type] res = invoke(fingerprinter, hashers) if res: response.matching_types.append(finger.fp_type) else: raise RuntimeError( "Encountered unknown fingerprint type. %s" % finger.fp_type) # Structure of the results is a list of dicts, each containing the # name of the hashing method, hashes for enabled hash algorithms, # and auxilliary data where present (e.g. signature blobs). # Also see Fingerprint:HashIt() response.results = fingerprinter.HashIt() # We now return data in a more structured form. for result in response.results: if result.GetItem("name") == "generic": for hash_type in ["md5", "sha1", "sha256"]: value = result.GetItem(hash_type) if value is not None: setattr(response.hash, hash_type, value) if result["name"] == "pecoff": for hash_type in ["md5", "sha1", "sha256"]: value = result.GetItem(hash_type) if value: setattr(response.hash, "pecoff_" + hash_type, value) signed_data = result.GetItem("SignedData", []) for data in signed_data: response.hash.signed_data.Append(revision=data[0], cert_type=data[1], certificate=data[2]) self.SendReply(response)
def testTSKFile(self): """Test our ability to read from image files.""" path = os.path.join(self.base_path, "test_img.dd") path2 = "Test Directory/numbers.txt" p2 = rdf_paths.PathSpec( path=path2, pathtype=rdf_paths.PathSpec.PathType.TSK) p1 = rdf_paths.PathSpec(path=path, pathtype=rdf_paths.PathSpec.PathType.OS) p1.Append(p2) fd = vfs.VFSOpen(p1) self.TestFileHandling(fd)
def open(self, directory=None, filename=None, mode="rb"): result = tempfiles.CreateGRRTempFile(filename=filename, mode=mode) # The tempfile library created an os path, we pass it through vfs to # normalize it. with vfs.VFSOpen( rdfvalue.PathSpec( path=result.name, pathtype=rdfvalue.PathSpec.PathType.OS)) as vfs_fd: dict_pathspec = vfs_fd.pathspec.ToPrimitiveDict() self.SendMessage(["file", dict_pathspec]) return result
def testVFSChroot(self): # Let's open a file in the chroot. os_root = "os:%s" % self.base_path with test_lib.ConfigOverrider({"Client.vfs_chroots": [os_root]}): # We need to reset the vfs.VFS_CHROOT too. vfs.VFSInit().Run() fd = vfs.VFSOpen( rdf_paths.PathSpec(path="/morenumbers.txt", pathtype=rdf_paths.PathSpec.PathType.OS)) data = fd.read(10) self.assertEqual(data, "1\n2\n3\n4\n5\n") # This should also work with TSK. tsk_root = "tsk:%s" % os.path.join(self.base_path, "test_img.dd") with test_lib.ConfigOverrider({"Client.vfs_chroots": [tsk_root]}): vfs.VFSInit().Run() image_file_ps = rdf_paths.PathSpec( path=u"איןד ןד ש אקדא/איןד.txt", pathtype=rdf_paths.PathSpec.PathType.TSK) fd = vfs.VFSOpen(image_file_ps) data = fd.read(10) self.assertEqual(data, "1\n2\n3\n4\n5\n") # This should not influence vfs handlers other than OS and TSK. reg_type = rdf_paths.PathSpec.PathType.REGISTRY os_handler = vfs.VFS_HANDLERS[rdf_paths.PathSpec.PathType.OS] try: vfs.VFS_HANDLERS[reg_type] = os_handler with self.assertRaises(IOError): image_file_ps.pathtype = reg_type vfs.VFSOpen(image_file_ps) finally: # Reset to whatever it was before this test. vfs.VFSInit().Run()
def testUnicodeFile(self): """Test ability to read unicode files from images.""" path = os.path.join(self.base_path, "test_img.dd") path2 = os.path.join(u"איןד ןד ש אקדא", u"איןד.txt") ps2 = rdf_paths.PathSpec( path=path2, pathtype=rdf_paths.PathSpec.PathType.TSK) ps = rdf_paths.PathSpec(path=path, pathtype=rdf_paths.PathSpec.PathType.OS) ps.Append(ps2) fd = vfs.VFSOpen(ps) self.TestFileHandling(fd)
def testRecursiveImages(self): """Test directory listing in sleuthkit.""" p3 = rdfvalue.PathSpec(path="/home/a.txt", pathtype=rdfvalue.PathSpec.PathType.TSK) p2 = rdfvalue.PathSpec(path="/home/image2.img", pathtype=rdfvalue.PathSpec.PathType.TSK) p1 = rdfvalue.PathSpec(path=os.path.join(self.base_path, "test_img.dd"), pathtype=rdfvalue.PathSpec.PathType.OS) p2.Append(p3) p1.Append(p2) f = vfs.VFSOpen(p1) self.assertEqual(f.read(3), "yay")
def ListDirectory(self, pathspec, state, depth=0): """A recursive generator of files.""" # Limit recursion depth if depth >= self.request.max_depth: return try: fd = vfs.VFSOpen(pathspec, progress_callback=self.Progress) files = fd.ListFiles() except (IOError, OSError) as e: if depth == 0: # We failed to open the directory the server asked for because dir # doesn't exist or some other reason. So we set status and return # back to the caller ending the Iterator. self.SetStatus(rdf_flows.GrrStatus.ReturnedStatus.IOERROR, e) else: # Can't open the directory we're searching, ignore the directory. logging.info("Find failed to ListDirectory for %s. Err: %s", pathspec, e) return # If we are not supposed to cross devices, and don't know yet # which device we are on, we need to find out. if not self.request.cross_devs and self.filesystem_id is None: dir_stat = fd.Stat() self.filesystem_id = dir_stat.st_dev # Recover the start point for this directory from the state dict so we can # resume. start = state.get(pathspec.CollapsePath(), 0) for i, file_stat in enumerate(files): # Skip the files we already did before if i < start: continue if stat.S_ISDIR(file_stat.st_mode): # Do not traverse directories in a different filesystem. if self.request.cross_devs or self.filesystem_id == file_stat.st_dev: for child_stat in self.ListDirectory(file_stat.pathspec, state, depth + 1): yield child_stat state[pathspec.CollapsePath()] = i + 1 yield file_stat # Now remove this from the state dict to prevent it from getting too large try: del state[pathspec.CollapsePath()] except KeyError: pass
def Run(self, args): """Hash a file.""" try: fd = vfs.VFSOpen(args.pathspec) hasher = hashlib.sha256() while True: data = fd.Read(1024 * 1024) if not data: break hasher.update(data) except (IOError, OSError), e: self.SetStatus(rdfvalue.GrrStatus.ReturnedStatus.IOERROR, e) return
def UploadFile(self, args): """Just copy the file into the filestore.""" file_fd = vfs.VFSOpen(args.pathspec) fs = file_store.FileUploadFileStore() fd = fs.CreateFileStoreFile() while True: data = file_fd.read(self.BUFFER_SIZE) if not data: break fd.write(data) file_id = fd.Finalize() return [rdf_client.UploadedFile(stat_entry=file_fd.Stat(), file_id=file_id)]
def testFileSizeOverride(self): # We assume /dev/null exists and has a 0 size. fname = "/dev/null" try: st = os.stat(fname) except OSError: self.skipTest("%s not accessible." % fname) if st.st_size != 0: self.skipTest("%s doesn't have 0 size." % fname) pathspec = rdf_paths.PathSpec( path=fname, pathtype="OS", file_size_override=100000000) fd = vfs.VFSOpen(pathspec) self.assertEqual(fd.size, 100000000)
def testGuessPathSpecPartial(self): """Test that we can guess a pathspec from a partial pathspec.""" path = os.path.join(self.base_path, "test_img.dd") pathspec = rdf_paths.PathSpec(path=path, pathtype=rdf_paths.PathSpec.PathType.OS) pathspec.nested_path.path = "/home/image2.img/home/a.txt" pathspec.nested_path.pathtype = rdf_paths.PathSpec.PathType.TSK fd = vfs.VFSOpen(pathspec) self.assertEqual(fd.read(3), "yay") # Open as a directory pathspec.nested_path.path = "/home/image2.img/home/" fd = vfs.VFSOpen(pathspec) names = [] for s in fd.ListFiles(): # Make sure that the stat pathspec is correct - it should be 3 levels # deep. self.assertEqual(s.pathspec.nested_path.path, "/home/image2.img") names.append(s.pathspec.nested_path.nested_path.path) self.assertTrue("home/a.txt" in names)
def testRegistryListing(self): pathspec = rdf_paths.PathSpec( pathtype=rdf_paths.PathSpec.PathType.REGISTRY, path=("/HKEY_USERS/S-1-5-20/Software/Microsoft" "/Windows/CurrentVersion/Run")) expected_names = {"MctAdmin": stat.S_IFDIR, "Sidebar": stat.S_IFDIR} expected_data = [u"%ProgramFiles%\\Windows Sidebar\\Sidebar.exe /autoRun", u"%TEMP%\\Sidebar.exe"] for f in vfs.VFSOpen(pathspec).ListFiles(): base, name = os.path.split(f.pathspec.CollapsePath()) self.assertEqual(base, pathspec.CollapsePath()) self.assertIn(name, expected_names) self.assertIn(f.registry_data.GetValue(), expected_data)
def testOpenFilehandles(self): """Test that file handles are cached.""" current_process = psutil.Process(os.getpid()) num_open_files = len(current_process.open_files()) path = os.path.join(self.base_path, "morenumbers.txt") fds = [] for _ in range(100): fd = vfs.VFSOpen( rdf_paths.PathSpec(path=path, pathtype=rdf_paths.PathSpec.PathType.OS)) self.assertEqual(fd.read(20), "1\n2\n3\n4\n5\n6\n7\n8\n9\n10") fds.append(fd) # This should not create any new file handles. self.assertTrue(len(current_process.open_files()) - num_open_files < 5)
def Run(self, args): hash_types = set() for t in args.tuples: for hash_name in t.hashers: hash_types.add(str(hash_name).lower()) with vfs.VFSOpen(args.pathspec, progress_callback=self.Progress) as file_obj: hashers, bytes_read = self.HashFile(hash_types, file_obj, args.max_filesize) self.SendReply( rdf_client.FingerprintResponse( pathspec=file_obj.pathspec, bytes_read=bytes_read, hash=rdf_crypto.Hash(**dict( (k, v.digest()) for k, v in hashers.iteritems()))))
def Run(self, args): hash_types = set() for t in args.tuples: for hash_name in t.hashers: hash_types.add(str(hash_name).lower()) hasher = client_utils_common.MultiHasher(hash_types, progress=self.Progress) with vfs.VFSOpen(args.pathspec, progress_callback=self.Progress) as fd: hasher.HashFile(fd, args.max_filesize) hash_object = hasher.GetHashObject() response = rdf_client.FingerprintResponse( pathspec=fd.pathspec, bytes_read=hash_object.num_bytes, hash=hash_object) self.SendReply(response)
def Run(self, args): file_fd = vfs.VFSOpen(args.pathspec, progress_callback=self.Progress) try: uploaded_file = self.grr_worker.UploadFile( file_fd, args.upload_token, session_id=self.session_id, progress_callback=self.Progress) except IOError as e: raise IOError("Unable to upload %s: %s" % (args.pathspec.CollapsePath(), e)) logging.debug("Uploaded %s", args.pathspec) uploaded_file.stat_entry = file_fd.Stat() self.SendReply(uploaded_file)
def Run(self, args): """Reads a buffer on the client and sends it to the server.""" # Make sure we limit the size of our output if args.length > constants.CLIENT_MAX_BUFFER_SIZE: raise RuntimeError("Can not read buffers this large.") try: fd = vfs.VFSOpen(args.pathspec, progress_callback=self.Progress) fd.Seek(args.offset) offset = fd.Tell() data = fd.Read(args.length) except (IOError, OSError), e: self.SetStatus(rdf_flows.GrrStatus.ReturnedStatus.IOERROR, e) return
def Iterate(self): """Run a Rekall plugin and return the result.""" # Open the device pathspec as requested by the server. with vfs.VFSOpen(self.request.device, progress_callback=self.Progress) as fhandle: # Create a session and run all the plugins with it. session_args = self.request.session.ToDict() # If the user has not specified a special profile path, we use the local # cache directory. if "profile_path" not in session_args: session_args["profile_path"] = [ config_lib.CONFIG["Client.rekall_profile_cache_path"] ] session_args.update(fhandle.GetMetadata()) rekal_session = GrrRekallSession(action=self, **session_args) # Wrap GRR's VFS handler for the device in a Rekall FDAddressSpace so we # can pass it directly to the Rekall session as the physical address # space. This avoids the AS voting mechanism for Rekall's image format # detection. with rekal_session: rekal_session.physical_address_space = standard.FDAddressSpace( session=rekal_session, fhandle=fhandle) # Autodetect the profile. Valid plugins for this profile will become # available now. rekal_session.GetParameter("profile") for plugin_request in self.request.plugins: # Get the keyword args to this plugin. plugin_args = plugin_request.args.ToDict() try: rekal_session.RunPlugin(plugin_request.plugin, **plugin_args) except Exception: # pylint: disable=broad-except # Just ignore errors, and run the next plugin. Errors will be reported # through the renderer. pass
def testNTFSProgressCallback(self): self.progress_counter = 0 def Progress(): self.progress_counter += 1 path = os.path.join(self.base_path, "ntfs_img.dd") path2 = "test directory" ps2 = rdf_paths.PathSpec( path=path2, pathtype=rdf_paths.PathSpec.PathType.TSK) ps = rdf_paths.PathSpec( path=path, pathtype=rdf_paths.PathSpec.PathType.OS, offset=63 * 512) ps.Append(ps2) vfs.VFSOpen(ps, progress_callback=Progress) self.assertTrue(self.progress_counter > 0)
def Run(self, args): """Read from a VFS file and write to a GRRTempFile on disk. If file writing doesn't complete files won't be cleaned up. Args: args: see CopyPathToFile in jobs.proto """ self.src_fd = vfs.VFSOpen(args.src_path, progress_callback=self.Progress) self.src_fd.Seek(args.offset) offset = self.src_fd.Tell() self.length = args.length or (1024**4) # 1 TB self.written = 0 suffix = ".gz" if args.gzip_output else "" self.dest_fd = tempfiles.CreateGRRTempFile(directory=args.dest_dir, lifetime=args.lifetime, suffix=suffix) self.dest_file = self.dest_fd.name with self.dest_fd: if args.gzip_output: gzip_fd = gzip.GzipFile(self.dest_file, "wb", 9, self.dest_fd) # Gzip filehandle needs its own close method called with gzip_fd: self._Copy(gzip_fd) else: self._Copy(self.dest_fd) pathspec_out = rdfvalue.PathSpec( path=self.dest_file, pathtype=rdfvalue.PathSpec.PathType.OS) self.SendReply(offset=offset, length=self.written, src_path=args.src_path, dest_dir=args.dest_dir, dest_path=pathspec_out, gzip_output=args.gzip_output)
def testTSKFileInode(self): """Test opening a file through an indirect pathspec.""" pathspec = rdfvalue.PathSpec( path=os.path.join(self.base_path, "test_img.dd"), pathtype=rdfvalue.PathSpec.PathType.OS) pathspec.Append(pathtype=rdfvalue.PathSpec.PathType.TSK, inode=12, path="/Test Directory") pathspec.Append(pathtype=rdfvalue.PathSpec.PathType.TSK, path="numbers.txt") fd = vfs.VFSOpen(pathspec) # Check that the new pathspec is correctly reduced to two components. self.assertEqual( fd.pathspec.first.path, os.path.normpath(os.path.join(self.base_path, "test_img.dd"))) self.assertEqual(fd.pathspec[1].path, "/Test Directory/numbers.txt") # And the correct inode is placed in the final branch. self.assertEqual(fd.Stat().pathspec.nested_path.inode, 15) self.TestFileHandling(fd)
def testRecursiveListNames(self): """Test our ability to walk over a directory tree.""" path = os.path.join(self.base_path, "a") directory = vfs.VFSOpen( rdf_paths.PathSpec(path=path, pathtype=rdf_paths.PathSpec.PathType.OS)) # Test the helper method self.assertEqual(directory._GetDepth("/"), 0) self.assertEqual(directory._GetDepth("/foo/bar/baz"), 3) # Relative paths aren't supported with self.assertRaises(RuntimeError): directory._GetDepth("foo/bar") # Multiple separators are redundant self.assertEqual(directory._GetDepth("/////foo///bar//////baz//"), 3) def Normalize(generator): """Normalize lists to be sorted for stable comparison.""" result = [] for row in generator: row[1].sort() row[2].sort() result.append(row) return result # Test the whole thing walk_tups_0 = Normalize(directory.RecursiveListNames()) walk_tups_1 = Normalize(directory.RecursiveListNames(depth=1)) walk_tups_inf = Normalize( directory.RecursiveListNames(depth=float("inf"))) self.assertEqual(walk_tups_0, [(path, ["b"], [])]) self.assertEqual(walk_tups_1, [(path, ["b"], []), ("%s/b" % path, ["c", "d"], [])]) self.assertEqual(walk_tups_inf, [(path, ["b"], []), ("%s/b" % path, ["c", "d"], []), ("%s/b/c" % path, [], ["helloc.txt"]), ("%s/b/d" % path, [], ["hellod.txt"])])
def Run(self, args): """Read from a VFS file and write to a GRRTempFile on disk. If file writing doesn't complete files won't be cleaned up. Args: args: see CopyPathToFile in jobs.proto """ src_fd = vfs.VFSOpen(args.src_path, progress_callback=self.Progress) src_fd.Seek(args.offset) offset = src_fd.Tell() length = args.length or (1024**4) # 1 TB suffix = ".gz" if args.gzip_output else "" dest_fd, dest_pathspec = tempfiles.CreateGRRTempFileVFS( directory=args.dest_dir, lifetime=args.lifetime, suffix=suffix) dest_file = dest_fd.name with dest_fd: if args.gzip_output: gzip_fd = gzip.GzipFile(dest_file, "wb", 9, dest_fd) # Gzip filehandle needs its own close method called with gzip_fd: written = self._Copy(src_fd, gzip_fd, length) else: written = self._Copy(src_fd, dest_fd, length) self.SendReply( rdf_client.CopyPathToFileRequest( offset=offset, length=written, src_path=args.src_path, dest_dir=args.dest_dir, dest_path=dest_pathspec, gzip_output=args.gzip_output))
def testTSKFileCasing(self): """Test our ability to read the correct casing from image.""" path = os.path.join(self.base_path, "test_img.dd") path2 = os.path.join("test directory", "NuMbErS.TxT") ps2 = rdf_paths.PathSpec( path=path2, pathtype=rdf_paths.PathSpec.PathType.TSK) ps = rdf_paths.PathSpec(path=path, pathtype=rdf_paths.PathSpec.PathType.OS) ps.Append(ps2) fd = vfs.VFSOpen(ps) # This fixes Windows paths. path = path.replace("\\", "/") # The pathspec should have 2 components. self.assertEqual(fd.pathspec.first.path, utils.NormalizePath(path)) self.assertEqual(fd.pathspec.first.pathtype, rdf_paths.PathSpec.PathType.OS) nested = fd.pathspec.last self.assertEqual(nested.path, u"/Test Directory/numbers.txt") self.assertEqual(nested.pathtype, rdf_paths.PathSpec.PathType.TSK)
def Run(self, args): """Run.""" # Open the file. fd = vfs.VFSOpen(args.pathspec, progress_callback=self.Progress) if args.address_family == rdf_client.NetworkAddress.Family.INET: family = socket.AF_INET elif args.address_family == rdf_client.NetworkAddress.Family.INET6: family = socket.AF_INET6 else: raise RuntimeError("Socket address family not supported.") s = socket.socket(family, socket.SOCK_STREAM) try: s.connect((args.host, args.port)) except socket.error as e: raise RuntimeError(str(e)) cipher = rdf_crypto.AES128CBCCipher(args.key, args.iv) streaming_encryptor = rdf_crypto.StreamingCBCEncryptor(cipher) while True: data = fd.read(self.BLOCK_SIZE) if not data: break self.Send(s, streaming_encryptor.Update(data)) # Send heartbeats for long files. self.Progress() self.Send(s, streaming_encryptor.Finalize()) s.close() self.SendReply(fd.Stat())
def testTSKNTFSHandling(self): """Test that TSK can correctly encode NTFS features.""" path = os.path.join(self.base_path, "ntfs_img.dd") path2 = "test directory" ps2 = rdf_paths.PathSpec(path=path2, pathtype=rdf_paths.PathSpec.PathType.TSK) ps = rdf_paths.PathSpec(path=path, pathtype=rdf_paths.PathSpec.PathType.OS, offset=63 * 512) ps.Append(ps2) fd = vfs.VFSOpen(ps) # This fixes Windows paths. path = path.replace("\\", "/") listing = [] pathspecs = [] for f in fd.ListFiles(): # Make sure the CASE_LITERAL option is set for all drivers so we can just # resend this proto back. self.assertEqual(f.pathspec.path_options, rdf_paths.PathSpec.Options.CASE_LITERAL) pathspec = f.pathspec.nested_path self.assertEqual(pathspec.path_options, rdf_paths.PathSpec.Options.CASE_LITERAL) pathspecs.append(f.pathspec) listing.append( (pathspec.inode, pathspec.ntfs_type, pathspec.ntfs_id)) # The tsk_fs_attr_type enum: tsk_fs_attr_type = rdf_paths.PathSpec.tsk_fs_attr_type ref = [(65, tsk_fs_attr_type.TSK_FS_ATTR_TYPE_DEFAULT, 0), (65, tsk_fs_attr_type.TSK_FS_ATTR_TYPE_NTFS_DATA, 4), (66, tsk_fs_attr_type.TSK_FS_ATTR_TYPE_DEFAULT, 0), (67, tsk_fs_attr_type.TSK_FS_ATTR_TYPE_DEFAULT, 0)] # Make sure that the ADS is recovered. self.assertEqual(listing, ref) # Try to read the main file self.assertEqual(pathspecs[0].nested_path.path, "/Test Directory/notes.txt") fd = vfs.VFSOpen(pathspecs[0]) self.assertEqual(fd.read(1000), "Hello world\n") s = fd.Stat() self.assertEqual(s.pathspec.nested_path.inode, 65) self.assertEqual(s.pathspec.nested_path.ntfs_type, 1) self.assertEqual(s.pathspec.nested_path.ntfs_id, 0) # Check that the name of the ads is consistent. self.assertEqual(pathspecs[1].nested_path.path, "/Test Directory/notes.txt") self.assertEqual(pathspecs[1].nested_path.stream_name, "ads") # Check that the ADS name is encoded correctly in the AFF4 URN for this # file. aff4_urn = aff4_grr.VFSGRRClient.PathspecToURN(pathspecs[1], "C.1234567812345678") self.assertEqual(aff4_urn.Basename(), "notes.txt:ads") fd = vfs.VFSOpen(pathspecs[1]) self.assertEqual(fd.read(1000), "I am a real ADS\n") # Test that the stat contains the inode: s = fd.Stat() self.assertEqual(s.pathspec.nested_path.inode, 65) self.assertEqual(s.pathspec.nested_path.ntfs_type, 128) self.assertEqual(s.pathspec.nested_path.ntfs_id, 4)