Skip to content

Conversation

@candleflip
Copy link

@candleflip candleflip commented Nov 20, 2025

Hi @ytti,

As a follow-up to the issue #3672 I opened earlier, here’s a PR that adds support for Ivanti Secure Connect (ICS) devices and extends the HTTP input with POST support.

Pre-Request Checklist

  • Passes rubocop code analysis (try rubocop --auto-correct)
  • Tests added or adapted (try rake test)
  • Changes are reflected in the documentation
  • User-visible changes appended to CHANGELOG.md

Description

1. Added new Ivanti Secure Connect model

The model is as thin as possible and delegates all HTTP mechanics to the transport layer as mentioned in related issue. The model implements the standard ICS API workflow:

  • POST /api/v1/realm_auth to obtain a temporary api_key
  • use that key as to make GET request
  • fetch and prepare configuration via /api/v1/system/binary-configuration

2. Added POST support in input/http.rb

ICS requires a POST before any config can be fetched, so adding post_http method felt like the right long-term solution. So the transport now:

  • handles POST the same way as GET
  • preserves custom authorization headers

3. Tests

Added tests for:

  • post_http (success, custom headers, nil body)
  • ICS model: Base64 handling logic

What it was tested on

Hardware: ISA6000-V
ICS version: Ivanti Secure Connect 22.x
(should also work across other ICS hardware/virtual platforms because API is consistent, but I haven't tested that cases)

Logs while testing on real device

Here are logs with enabled debug setting in config and vulnerable data replaced with <its-description>:

2025-11-20 16:12:55.389322 I [11:1140] Oxidized::CLI -- Oxidized starting, running as pid 11
2025-11-20 16:12:55.390047 I [11:1140] Oxidized::Nodes -- Loading nodes
2025-11-20 16:12:55.390875 D [11:1140] Oxidized::Node -- resolving DNS for <node>...
2025-11-20 16:12:55.390883 D [11:1140] Oxidized::Node -- IPADDR <ip>
2025-11-20 16:12:55.390951 D [11:1140] Oxidized::Node -- resolving node key 'model', with passed global value of '' and node value 'ivanti'
2025-11-20 16:12:55.390955 D [11:1140] Oxidized::Node -- setting node key 'model' to value 'ivanti' from node
2025-11-20 16:12:55.390960 D [11:1140] Oxidized::Node -- Loading model "ivanti"
2025-11-20 16:12:55.394999 D [11:1140] Ivanti -- Added /api/v1/system/binary-configuration to the commands list
2025-11-20 16:12:55.395065 D [11:1140] Oxidized::Node -- resolving node key 'input', with passed global value of 'ssh, http' and node value ''
2025-11-20 16:12:55.395080 D [11:1140] Oxidized::Node -- setting node key 'input' to value 'ssh, http' from passed global value
2025-11-20 16:12:55.480379 D [11:1140] Oxidized::Node -- resolving node key 'output', with passed global value of 'git' and node value ''
2025-11-20 16:12:55.480408 D [11:1140] Oxidized::Node -- setting node key 'output' to value 'git' from passed global value
2025-11-20 16:12:55.495844 D [11:1140] Oxidized::Node -- resolving node key 'username', with passed global value of '' and node value ''
2025-11-20 16:12:55.495873 D [11:1140] Oxidized::Node -- setting node key 'username' to value '<username>' from global
2025-11-20 16:12:55.495877 D [11:1140] Oxidized::Node -- resolving node key 'password', with passed global value of '' and node value ''
2025-11-20 16:12:55.495884 D [11:1140] Oxidized::Node -- setting node key 'password' to value '<password>' from global
2025-11-20 16:12:55.495895 D [11:1140] Oxidized::Node -- resolving node key 'timeout', with passed global value of '240' and node value ''
2025-11-20 16:12:55.495902 D [11:1140] Oxidized::Node -- setting node key 'timeout' to value '240' from passed global value
2025-11-20 16:12:55.495949 I [11:1140] Oxidized::Nodes -- Loaded 1 nodes
2025-11-20 16:12:55.496015 W [11:1140] Oxidized::Core -- configuration: "rest" is deprecated. Migrate to "extensions.oxidized-web" and remove "rest" from the configuration
2025-11-20 16:12:55.618993 D [11:1140] Oxidized::Core -- Starting the worker...
2025-11-20 16:12:55.619035 D [11:1140] Oxidized::Worker -- Jobs running: 0 of 1 - ended: 0 of 1
2025-11-20 16:12:55.619112 D [11:1140] Oxidized::Worker -- Added <location>/<node> to the job queue
2025-11-20 16:12:55.619124 D [11:1140] Oxidized::Worker -- 1 jobs running in parallel
2025-11-20 16:12:55.619389 D [11:Job '<node>'] Oxidized::Job -- Starting fetching process for <node>
2025-11-20 16:12:55.619593 D [11:Job '<node>'] Oxidized -- vars.rb: scope model has key realm with value <realm>, using scope
2025-11-20 16:12:55.619732 D [11:Job '<node>'] Oxidized::HTTP -- Making POST request to: https://<ip>/api/v1/realm_auth
2025-11-20 16:12:55.634313 I [11:2120] Oxidized::API::Web -- Oxidized-web server listening on 0.0.0.0:8888
2025-11-20 16:12:56.491837 D [11:Job '<node>'] Oxidized::HTTP -- Sending POST request with headers: {"Content-Type"=>"application/json", "Authorization"=>"Basic <base64(username:password)>"}
2025-11-20 16:12:56.623633 D [11:1140] Oxidized::Worker -- 1 jobs running in parallel
2025-11-20 16:12:57.629533 D [11:1140] Oxidized::Worker -- 1 jobs running in parallel
2025-11-20 16:12:57.727933 D [11:Job '<node>'] Oxidized::HTTP -- Response code: 200
2025-11-20 16:12:57.728224 D [11:Job '<node>'] Oxidized -- Obtained api_key XXXX... (len=44)
2025-11-20 16:12:57.728284 D [11:Job '<node>'] Oxidized::HTTP -- Running post_login commands at <node>
2025-11-20 16:12:57.728320 D [11:Job '<node>'] Ivanti -- Collecting commands' outputs
2025-11-20 16:12:57.728357 D [11:Job '<node>'] Ivanti -- Executing /api/v1/system/binary-configuration
2025-11-20 16:12:57.728663 D [11:Job '<node>'] Oxidized::HTTP -- Making GET request to: https://<ip>/api/v1/system/binary-configuration
2025-11-20 16:12:58.594701 D [11:Job '<node>'] Oxidized::HTTP -- Sending GET request with headers: {}
2025-11-20 16:12:58.635389 D [11:1140] Oxidized::Worker -- 1 jobs running in parallel
2025-11-20 16:12:59.031172 D [11:Job '<node>'] Oxidized::HTTP -- Falling back to Basic authentication
2025-11-20 16:12:59.637206 D [11:1140] Oxidized::Worker -- 1 jobs running in parallel
2025-11-20 16:12:59.918098 D [11:Job '<node>'] Oxidized::HTTP -- Sending GET request with headers: {"Authorization"=>"Basic <base64(api_key)>"}
2025-11-20 16:13:00.638516 D [11:1140] Oxidized::Worker -- 1 jobs running in parallel
...
2025-11-20 16:15:49.192087 D [31:1140] Oxidized::Worker -- 1 jobs running in parallel
2025-11-20 16:15:49.674533 D [31:Job '<node>'] Oxidized::HTTP -- Response code: 200
2025-11-20 16:15:49.675211 D [31:Job '<node>'] Oxidized -- vars.rb: scope vars has key remove_secret with value false, using scope
2025-11-20 16:15:51.088333 D [31:1140] Oxidized::Worker -- 1 jobs running in parallel
2025-11-20 16:15:51.979092 D [31:Job '<node>'] Oxidized::Node -- Oxidized::HTTP ran for <node> successfully
2025-11-20 16:15:51.979152 D [31:Job '<node>'] Oxidized::Job -- Config fetched for <node>
2025-11-20 16:16:00.210432 I [31:1140] Oxidized::Worker -- Configuration updated for <location>/<node>
2025-11-20 16:16:00.210480 D [31:1140] Oxidized::Worker -- Jobs running: 0 of 1 - ended: 1 of 1
2025-11-20 16:16:00.210520 D [31:1140] Oxidized::Worker -- Running :nodes_done hook
2025-11-20 16:16:01.211274 D [31:1140] Oxidized::Worker -- Jobs running: 0 of 1 - ended: 0 of 1
...

Closes issue #3672

@candleflip candleflip changed the title Feat ivanti secure connect model Add Ivanti Secure Connect model Nov 20, 2025
Copy link
Collaborator

@robertcheramy robertcheramy left a comment

Choose a reason for hiding this comment

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

Thank you for this PR. I'm not too familiar with the http input, but your PR looks good to me.
Could you please take a look at my comments and address them where applicable?

Comment on lines 91 to 95
req_class = case method.to_s.downcase.to_sym
when :post then Net::HTTP::Post
else Net::HTTP::Get
end
req = req_class.new(uri)
Copy link
Collaborator

Choose a reason for hiding this comment

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

  • Maybe you could document allowed methods in docs/Inputs.md (:get and :post), so there is no need to to_s.downcase.to_sym?
  • Using if method == :post / else seems simplier to me than case.

Copy link
Author

Choose a reason for hiding this comment

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

I agree, just added if-elsif-else with raise for greater clarity and to avoid potential difficulties during debugging since only two methods are currently supported

Comment on lines 21 to 22
login_user = vars(:username) || @node.auth[:username]
login_pass = vars(:password) || @node.auth[:password]
Copy link
Collaborator

Choose a reason for hiding this comment

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

Isn't this redundant to just using @node.auth?

Copy link
Author

Choose a reason for hiding this comment

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

Yes, I also simplified this block

@candleflip
Copy link
Author

Hello @robertcheramy,
Thank you for useful comments!
I think I'll have time this week or next one to answer each of them. Feel free to add more

@candleflip
Copy link
Author

Hello @robertcheramy,
I finally found time to resolve all issues in comments. Thank you once again. Please check as time permits, I've added a few new comments.
I'd be happy to discuss any new questions or suggestions.

@robertcheramy
Copy link
Collaborator

I will review this PR before the Release of 0.36. I'm currently spending my available ressources on closing the issues on the roadmap.

@robertcheramy robertcheramy self-assigned this Feb 12, 2026
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