Skip to content

Added Ivanti Connect/Pulse Secure VC0 logs parser#5070

Merged
joachimmetz merged 11 commits into
log2timeline:mainfrom
julianghill:ivanti-vc0-parser
Jul 1, 2026
Merged

Added Ivanti Connect/Pulse Secure VC0 logs parser#5070
joachimmetz merged 11 commits into
log2timeline:mainfrom
julianghill:ivanti-vc0-parser

Conversation

@julianghill

@julianghill julianghill commented May 2, 2026

Copy link
Copy Markdown
Contributor

One line description of pull request

Add parser for Ivanti Connect Secure VC0 runtime logs.

Description:

Adds a parser for Ivanti Connect Secure / Pulse Secure .vc0 log files.

The parser extracts fields including:

  • timestamp
  • hostname
  • username
  • IP address
  • realm
  • message code
  • log file type
  • record identifier
  • original record values
  • short formatter body

Some .vc0 files only contain the file header and no log records yet. The parser treats those as valid empty log files.

The tests use small fake .vc0 records created in the test code, so no real Ivanti appliance logs or case data are included. Did test it on real data, but obvisously cant include it here.

Related issue (if applicable): N/A

Notes:

All contributions to Plaso undergo code review.
This makes sure that the code has appropriate test coverage and conforms to the
Plaso style guide.

One of the maintainers will examine your code, and may request changes. Check off the items below in
order, and then a maintainer will review your code.

Checklist:

  • No new new dependencies are required or l2tdevtools has been updated.
  • Test data has a Plaso compatible license. If the test data was not authored by you (the contributor), make sure to mention its orginal source in ACKNOWLEDGEMENTS.
  • Reviewer assigned.
  • Automated checks (GitHub Actions, AppVeyor) pass.

@joachimmetz joachimmetz force-pushed the ivanti-vc0-parser branch from 295f007 to 2283e09 Compare May 6, 2026 19:19
@joachimmetz

Copy link
Copy Markdown
Member

Rebased with HEAD

@joachimmetz joachimmetz self-assigned this May 6, 2026
@codecov

codecov Bot commented May 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 87.57764% with 20 lines in your changes missing coverage. Please review.
✅ Project coverage is 85.09%. Comparing base (3ea94cf) to head (483526c).
⚠️ Report is 5 commits behind head on main.

Files with missing lines Patch % Lines
plaso/parsers/ivanti_vc0.py 87.57% 20 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #5070      +/-   ##
==========================================
+ Coverage   85.07%   85.09%   +0.02%     
==========================================
  Files         454      455       +1     
  Lines       40221    40393     +172     
==========================================
+ Hits        34217    34374     +157     
- Misses       6004     6019      +15     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@joachimmetz joachimmetz force-pushed the ivanti-vc0-parser branch from 2283e09 to 22419b8 Compare May 7, 2026 08:22
Comment thread plaso/parsers/ivanti_vc0.py Outdated
Comment thread plaso/parsers/ivanti_vc0.py Outdated
Comment thread plaso/parsers/ivanti_vc0.py Outdated
Comment thread tests/parsers/ivanti_vc0.py Outdated
@joachimmetz

Copy link
Copy Markdown
Member

@julianghill thanks for the proposed changes, overall they look good. I left some question/comments.

@joachimmetz joachimmetz added the pending reporter input Issue is pending input from the reporter label May 8, 2026
@joachimmetz

Copy link
Copy Markdown
Member

@julianghill can you rebase with HEAD there are merge conflicts

@joachimmetz joachimmetz added this to the Parking/Temporary release milestone May 12, 2026
@joachimmetz joachimmetz removed the pending reporter input Issue is pending input from the reporter label May 16, 2026
@joachimmetz

Copy link
Copy Markdown
Member

Rebased with HEAD

@joachimmetz joachimmetz changed the title Add parser for Ivanti Connect/Pulse Secure VC0 logs Added Ivanti Connect/Pulse Secure VC0 logs parser May 17, 2026

@joachimmetz joachimmetz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Heads up, I've made some changes to address some of these already

Comment thread plaso/parsers/ivanti_vc0.py Outdated
hex_timestamp.hex_line_identifier.
record_values (str): original tab-separated values after the realm field.
recorded_time (dfdatetime.DateTimeValues): record timestamp.
source_filename (str): name of the source log file.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that Plaso already track this using the event_data_source attribute container

Comment thread plaso/parsers/ivanti_vc0.py Outdated
self._ExtractIPAddress(fields[5]) if len(fields) > 5 else None
)
event_data.line_identifier = line_identifier
event_data.log_file_type = self._ExtractLogFileType(source_filename)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given this is dependent on the filename it can be moved to the main parsing loop and only extracted once

Comment thread plaso/parsers/ivanti_vc0.py Outdated
if not file_entry:
return False

return bool(self._FILENAME_RE.match(file_entry.name.lower()))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lower() can be replaced by adding flags=re.IGNORECASE to re.compile()

Comment thread plaso/parsers/ivanti_vc0.py Outdated
_NON_PRINTABLE_CHARACTER_TRANSLATION_TABLE.pop(ord("\n"), None)
_NON_PRINTABLE_CHARACTER_TRANSLATION_TABLE.pop(ord("\t"), None)

_RECORD_SEPARATOR_RE = re.compile(r"[\x17\x15\x13\x12\x05\x04\x03\x02\x01\x00]")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can be simplified to r"[\x00-\x05\x12\x13\15\x17]"

Comment thread plaso/parsers/ivanti_vc0.py Outdated
"""
file_object.seek(self._HEADER_SIZE)

decoder = codecs.getincrementaldecoder("utf-8")(errors="replace")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why the replace? is this because incremental decoding?

Comment thread plaso/parsers/ivanti_vc0.py Outdated
if record_buffer:
text = "".join([record_buffer, text])

records = text.split("\n")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not use self._RECORD_SEPARATOR_RE.split() instead of translating the separators to newlines first?

Comment thread plaso/parsers/ivanti_vc0.py Outdated

record_values = fields[5:]
event_data.record_values = "\t".join(record_values) or None
event_data.body = self._CreateBody(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why is the body not fields[7] or fields[7:] ?

Comment thread plaso/parsers/ivanti_vc0.py Outdated

match = self._LOG_FILENAME_RE.match(source_filename)
if match:
return match.group("log_file_type")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

missing lower()? in case the filename is in upper case

@joachimmetz

Copy link
Copy Markdown
Member

@julianghill PTAL I made some changes. Some more example files would be beneficial, the current parser does a lot of text scraping, which makes it fragile

Comment thread plaso/parsers/ivanti_vc0.py Outdated
event_data.record_identifier = record_identifier

if number_of_fields > 4:
event_data.realm = fields[4] or None

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How certain are you about these values, given other implementations seem to refer to these in a more generic way. Or is this something specific to the "events" log type?

Comment thread plaso/parsers/ivanti_vc0.py Outdated
f"as UTF-8"
)
return
record = record_data.decode("utf-8", errors="replace")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IMHO this is not forensically sound. You are changing data without any notice, better to err and debug what is wrong

@julianghill

Copy link
Copy Markdown
Contributor Author

Adjustment made.
I renamed field [4] from realm to log_context to avoid assuming it is always
an Ivanti realm across all VC0 log types.

For field [5], the samples checked show it is either empty or an IP address. The
parser now keeps this conservative: it only sets ip_address when the value
validates as an IP address, and leaves it unset when the field is empty.

The fields after that vary by log type and message code, so they remain in
body instead of adding more specific fields that are not proven across all log
types.

I also added small test logs. They are based on the observed layouts, including empty field [5]/[6] seen in the real logs.
policytrace is kept header-only because the available policytrace VC0 files from incidents were header-only as well so hopefully we can get more in the future :)

@julianghill julianghill requested a review from joachimmetz May 29, 2026 14:15
@joachimmetz

Copy link
Copy Markdown
Member

@julianghill thanks for the updates, left one comment already, but will take a closer look tomorrow.

Comment thread plaso/parsers/ivanti_vc0.py Outdated
)
_NON_PRINTABLE_CHARACTER_TRANSLATION_TABLE.pop(ord("\t"), None)

_RECORD_SEPARATORS_RE = re.compile(b"[\\x00-\\x05\\x0a\\x12\\x13\\x15\\x17]")

@joachimmetz joachimmetz Jul 1, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the test files only use 0x05 as a record separator. Any test files from which can be observed that other separator values are also used?

Comment thread tests/parsers/ivanti_vc0.py Outdated
self.CheckEventData(event_data, expected_event_values)

def testParseWithEmptyVC0File(self):
"""Tests the Parse function with an empty .vc0 file."""

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that log.policytrace.vc0 is also empty, which makes this test redundant

Comment thread plaso/parsers/ivanti_vc0.py Outdated
hostname (str): appliance hostname.
ip_address (str): IP address found in the record values.
line_number (str): line number.
log_context (str): context value found before the record values.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@joachimmetz joachimmetz left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

@joachimmetz joachimmetz merged commit 680976f into log2timeline:main Jul 1, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants