Skip to content

Commit f3bf586

Browse files
authored
Change the iis plugin log_dirs to check for the presence of W3SVC* directories (#1460)
The previous option went through too many log files, even those that are not compatible with the IIS plugin. So this commit makes it more specific. The commit also includes: - Continuing to the next file if it fails to parse a line - Skip trying to parse the config file if it doesn't exist - Avoid a possibility of calling a None value instead of a function
1 parent 4b7bddc commit f3bf586

File tree

2 files changed

+62
-35
lines changed

2 files changed

+62
-35
lines changed

dissect/target/plugins/apps/webserver/iis.py

Lines changed: 40 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -66,12 +66,11 @@ class IISLogsPlugin(WebserverPlugin):
6666

6767
APPLICATION_HOST_CONFIG = "%windir%/system32/inetsrv/config/applicationHost.config"
6868

69-
DEFAULT_LOG_PATHS = (
70-
"%windir%\\System32\\LogFiles\\W3SVC*\\*.log",
71-
"sysvol\\Windows.old\\Windows\\System32\\LogFiles\\W3SVC*\\*.log",
72-
"sysvol\\inetpub\\logs\\LogFiles\\*.log",
73-
"sysvol\\inetpub\\logs\\LogFiles\\W3SVC*\\*.log",
74-
"sysvol\\Resources\\Directory\\*\\LogFiles\\Web\\W3SVC*\\*.log",
69+
DEFAULT_LOG_DIRS = (
70+
"%windir%\\System32\\LogFiles\\W3SVC*",
71+
"sysvol\\Windows.old\\Windows\\System32\\LogFiles\\W3SVC*",
72+
"sysvol\\inetpub\\logs\\LogFiles",
73+
"sysvol\\Resources\\Directory\\*\\LogFiles\\Web\\W3SVC*",
7574
)
7675

7776
__namespace__ = "iis"
@@ -91,28 +90,12 @@ def log_dirs(self) -> dict[str, set[Path]]:
9190
if (sysvol_files := self.target.fs.path("sysvol/files")).exists():
9291
dirs["auto"].add(sysvol_files)
9392

94-
try:
95-
xml_data = ElementTree.fromstring(self.config.read_bytes(), forbid_dtd=True)
96-
for log_file_element in xml_data.findall("*/sites/*/logFile"):
97-
log_format = log_file_element.get("logFormat") or "W3C"
98-
if log_dir := log_file_element.get("directory"):
99-
if log_format not in dirs:
100-
self.target.log.warning("Unsupported log format %s, skipping %s", log_format, log_dir)
101-
continue
102-
dirs[log_format].add(self.target.resolve(log_dir))
103-
104-
except (ElementTree.ParseError, FileNotFoundError) as e:
105-
self.target.log.warning("Error while parsing %s", self.config)
106-
self.target.log.debug("", exc_info=e)
93+
if self.config.exists():
94+
self._read_config_log_paths(dirs)
10795

108-
for log_path in self.DEFAULT_LOG_PATHS:
109-
try:
110-
# later on we use */*.log to collect the files, so we need to move up 2 levels
111-
log_path = self.target.expand_env(log_path)
112-
log_dir = self.target.fs.path(log_path).parents[1]
113-
except IndexError:
114-
self.target.log.info("Incompatible path found: %s", log_path)
115-
continue
96+
for log_dir in self.DEFAULT_LOG_DIRS:
97+
log_dir = self.target.expand_env(log_dir)
98+
log_dir = self.target.fs.path(log_dir)
11699

117100
if not has_glob_magic(str(log_dir)) and log_dir.exists():
118101
dirs["auto"].add(log_dir)
@@ -132,6 +115,21 @@ def _get_paths(self) -> Iterator[Path]:
132115
def _get_auxiliary_paths(self) -> Iterator[Path]:
133116
yield from {self.config}
134117

118+
def _read_config_log_paths(self, dirs: dict[str, set[str]]) -> None:
119+
try:
120+
xml_data = ElementTree.fromstring(self.config.read_bytes(), forbid_dtd=True)
121+
for log_file_element in xml_data.findall("*/sites/*/logFile"):
122+
log_format = log_file_element.get("logFormat") or "W3C"
123+
if log_dir := log_file_element.get("directory"):
124+
if log_format not in dirs:
125+
self.target.log.warning("Unsupported log format %s, skipping %s", log_format, log_dir)
126+
continue
127+
dirs[log_format].add(self.target.resolve(log_dir))
128+
129+
except (ElementTree.ParseError, FileNotFoundError) as e:
130+
self.target.log.warning("Error while parsing %s", self.config)
131+
self.target.log.debug("", exc_info=e)
132+
135133
@export(record=BasicRecordDescriptor)
136134
def logs(self) -> Iterator[TargetRecordDescriptor]:
137135
"""Return contents of IIS (v7 and above) log files.
@@ -142,22 +140,28 @@ def logs(self) -> Iterator[TargetRecordDescriptor]:
142140
Supported log formats: IIS, W3C.
143141
"""
144142

143+
# We handle direct files here because _get_paths cannot select (filter) on the type of logfile.
144+
if self.target.is_direct:
145+
for log_file in self.get_paths():
146+
yield from parse_autodetect_format_log(self.target, log_file)
147+
# If we use the direct loader, there are no other files available.
148+
return
149+
145150
parsers = {
146151
"W3C": parse_w3c_format_log,
147152
"IIS": parse_iis_format_log,
148153
"auto": parse_autodetect_format_log,
149154
}
150155

151-
for format in ("IIS", "W3C", "auto"):
156+
for format, parser in parsers.items():
152157
for log_dir in self.log_dirs.get(format, ()):
153-
for log_file in log_dir.glob("*/*.log"):
158+
for log_file in log_dir.rglob("*.log"):
154159
self.target.log.info("Processing IIS log file %s in %s format", log_file, format)
155-
yield from parsers[format](self.target, log_file)
156-
157-
# We handle direct files here because _get_paths cannot select (filter) on the type of logfile.
158-
if self.target.is_direct:
159-
for log_file in self.get_paths():
160-
yield from parse_autodetect_format_log(self.target, log_file)
160+
try:
161+
yield from parser(self.target, log_file)
162+
except Exception as e:
163+
self.target.log.error("Issue processing log file %s in %s format", log_file, format) # noqa TRY400
164+
self.target.log.debug("", exc_info=e)
161165

162166
@export(record=WebserverAccessLogRecord)
163167
def access(self) -> Iterator[WebserverAccessLogRecord]:
@@ -245,6 +249,7 @@ def parse_w3c_format_log(target: Target, path: Path) -> Iterator[TargetRecordDes
245249

246250
if not record_descriptor:
247251
target.log.warning("Comment line with the fields defined should come before the values, skipping: %r", line)
252+
continue
248253

249254
raw = replace_dash_with_none(dict(zip(fields, values, strict=False)))
250255

tests/plugins/apps/webserver/test_iis.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
from __future__ import annotations
22

3+
import logging
34
from datetime import datetime, timedelta, timezone
5+
from io import BytesIO
46
from pathlib import Path
57
from typing import TYPE_CHECKING
68
from unittest.mock import mock_open, patch
@@ -219,6 +221,26 @@ def test_iis_access_noconfig(
219221
assert len(results) > 0
220222

221223

224+
def test_iis_failed_to_parse_log(
225+
target_win_tzinfo: Target,
226+
fs_win: VirtualFilesystem,
227+
caplog: pytest.LogCaptureFixture,
228+
) -> None:
229+
map_dir = "inetpub/logs/LogFiles/W3SVC1"
230+
231+
data_dir = absolute_path("_data/plugins/apps/webserver/iis/iis-logs-w3c/W3SVC1")
232+
fs_win.map_dir(map_dir, data_dir)
233+
fs_win.map_file_fh(f"{map_dir}/01_logfile.log", BytesIO(b"a,b,c,d,e,f,g,h,i,j,k,l,m,n,o,p"))
234+
target_win_tzinfo.add_plugin(iis.IISLogsPlugin)
235+
with caplog.at_level(logging.ERROR, target_win_tzinfo.log.name):
236+
assert list(target_win_tzinfo.iis.access())
237+
238+
assert any(
239+
f"Issue processing log file sysvol/{map_dir}/01_logfile.log in auto format" in message
240+
for message in caplog.messages
241+
)
242+
243+
222244
def test_iis_direct_mode() -> None:
223245
data_path = absolute_path("_data/plugins/apps/webserver/iis/iis-logs-iis/W3SVC1/u_in211001.log")
224246

0 commit comments

Comments
 (0)