This is an automated email from the ASF dual-hosted git repository.

hansva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/hop.git


The following commit(s) were added to refs/heads/main by this push:
     new fa1110b08f issue #7182 : Hop Password Variable Resolver for encoded 
passwords (#7496)
fa1110b08f is described below

commit fa1110b08f6d2a597970618f1d98c9fe7a428842
Author: Matt Casters <[email protected]>
AuthorDate: Wed Jul 15 14:23:15 2026 +0200

    issue #7182 : Hop Password Variable Resolver for encoded passwords (#7496)
    
    * issue #7182 : Hop Password Variable Resolver for encoded passwords
    
    Add a technology-agnostic variable resolver that decrypts Hop-encoded
    passwords (Encrypted / AES2) for use in expressions such as VFS URIs.
    Supports resolving the argument as a variable name, optional fail-if-
    undefined, documentation, unit tests, and integration-tests/resolver.
    
    * issue #7182 : quote MessageFormat braces in password resolver i18n
    
    * issue #7182 : properly quote MessageFormat literals in password resolver 
i18n
    
    Wrap #{...} expressions in single quotes so MessageFormat treats them as
    literal text instead of format arguments.
    
    * issue #7182 : re-bind Encr in password encoder tests to avoid surefire 
pollution
    
    HopClientEnvironment only initializes Encr once. Force Encr.init(AES/Hop)
    in @BeforeEach so tests do not pick up the wrong encoder or AES key from a
    previous test class when surefire reuses the JVM.
    
    * issue #7182 : use Const import in password resolver test setup
---
 docs/hop-user-manual/modules/ROOT/nav.adoc         |   1 +
 .../hop-password-variable-resolver.adoc            |  88 +++
 .../metadata-types/variable-resolver/index.adoc    |   1 +
 .../resolver/0001-pipeline-get-configurations.hpl  | 200 ++++++
 .../resolver/0001-pipeline-resolver-test.hpl       | 705 +++++++++++++++++++++
 .../resolver/0002-hop-password-variable.hpl        |  94 +++
 .../resolver/0003-hop-password-literal.hpl         |  94 +++
 .../0004-hop-password-missing-variable.hpl         |  94 +++
 .../datasets/0001-golden-pipeline-resolver.csv     |   4 +
 .../datasets/0002-golden-hop-password-variable.csv |   2 +
 .../datasets/0003-golden-hop-password-literal.csv  |   2 +
 integration-tests/resolver/dev-env-config.json     |  11 +
 integration-tests/resolver/files/db1.properties    |  23 +
 integration-tests/resolver/files/db2.properties    |  23 +
 integration-tests/resolver/files/db3.properties    |  24 +
 integration-tests/resolver/hop-config.json         | 290 +++++++++
 .../resolver/main-0001-pipeline-resolver.hwf       |  79 +++
 .../resolver/main-0002-hop-password-variable.hwf   |  79 +++
 .../resolver/main-0003-hop-password-literal.hwf    |  79 +++
 .../main-0004-hop-password-missing-variable.hwf    | 146 +++++
 .../dataset/0001-golden-pipeline-resolver.json     |  80 +++
 .../dataset/0002-golden-hop-password-variable.json |  16 +
 .../dataset/0003-golden-hop-password-literal.json  |  16 +
 .../metadata/pipeline-run-configuration/local.json |  20 +
 .../0001-pipeline-resolver-test UNIT.json          |  68 ++
 .../unit-test/0002-hop-password-variable UNIT.json |  28 +
 .../unit-test/0003-hop-password-literal UNIT.json  |  28 +
 .../resolver/metadata/variable-resolver/conf.json  |  13 +
 .../variable-resolver/hop-pwd-literal.json         |  11 +
 .../metadata/variable-resolver/hop-pwd-strict.json |  11 +
 .../metadata/variable-resolver/hop-pwd.json        |  11 +
 .../metadata/workflow-run-configuration/local.json |   9 +
 integration-tests/resolver/project-config.json     |  15 +
 .../resolver/HopPasswordVariableResolver.java      | 139 ++++
 .../resolver/messages/messages_en_US.properties    |  21 +
 .../resolver/HopPasswordVariableResolverTest.java  | 141 +++++
 36 files changed, 2666 insertions(+)

diff --git a/docs/hop-user-manual/modules/ROOT/nav.adoc 
b/docs/hop-user-manual/modules/ROOT/nav.adoc
index 43c7a9a476..7afae79f25 100644
--- a/docs/hop-user-manual/modules/ROOT/nav.adoc
+++ b/docs/hop-user-manual/modules/ROOT/nav.adoc
@@ -466,6 +466,7 @@ under the License.
 *** 
xref:metadata-types/variable-resolver/azure-key-vault-variable-resolver.adoc[Azure
 Key Vault variable resolver]
 *** 
xref:metadata-types/variable-resolver/google-secret-manager-variable-resolver.adoc[Google
 Secret Manager variable resolver]
 *** 
xref:metadata-types/variable-resolver/hashicorp-vault-variable-resolver.adoc[Hashicorp
 Vault variable resolver]
+*** 
xref:metadata-types/variable-resolver/hop-password-variable-resolver.adoc[Hop 
Password variable resolver]
 *** 
xref:metadata-types/variable-resolver/openbao-variable-resolver.adoc[OpenBAO 
variable resolver]
 *** 
xref:metadata-types/variable-resolver/pipeline-variable-resolver.adoc[Pipeline 
variable resolver]
 ** xref:metadata-types/static-schema-definition.adoc[Static Schema Definition]
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/variable-resolver/hop-password-variable-resolver.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/variable-resolver/hop-password-variable-resolver.adoc
new file mode 100644
index 0000000000..84e6b9be16
--- /dev/null
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/variable-resolver/hop-password-variable-resolver.adoc
@@ -0,0 +1,88 @@
+////
+Licensed to the Apache Software Foundation (ASF) under one
+or more contributor license agreements.  See the NOTICE file
+distributed with this work for additional information
+regarding copyright ownership.  The ASF licenses this file
+to you under the Apache License, Version 2.0 (the
+"License"); you may not use this file except in compliance
+with the License.  You may obtain a copy of the License at
+  http://www.apache.org/licenses/LICENSE-2.0
+Unless required by applicable law or agreed to in writing,
+software distributed under the License is distributed on an
+"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+KIND, either express or implied.  See the License for the
+specific language governing permissions and limitations
+under the License.
+////
+[[HopPasswordVariableResolver]]
+:imagesdir: ../assets/images
+:openvar: #{
+:closevar: }
+:description: Decrypt Hop-encoded passwords in variable expressions (for 
example VFS / SFTP URIs)
+
+== Functionality
+
+Some places in Hop accept a free-form string (for example a VFS URI with a 
password in the userinfo section) and never call the password decoder 
themselves.
+If you put a Hop-encoded value such as `Encrypted ...` or `AES2 ...` directly 
in that string, the backend receives the encoded text instead of the real 
password.
+
+The *Hop Password Variable Resolver* bridges that gap: it calls the same 
decoder used for connection passwords 
(`Encr.decryptPasswordOptionallyEncrypted`) so you can keep encoded secrets in 
environment or project variables and still use them where only a plain string 
is accepted.
+
+IMPORTANT: This resolver is an *interoperability* helper for trusted 
operators. It is *not* a confidentiality feature. The default Hop password 
encoder is obfuscation (reversible with a public seed). For real protection at 
rest, use the xref:password/passwords/aespasswords.adoc[AES2 password encoder] 
and/or a secrets-manager resolver (Vault, Azure Key Vault, Google Secret 
Manager).
+
+== Options
+
+* *Resolve argument as a variable name* (recommended, enabled by default): the 
text after the colon is a *variable name*. Hop looks up that variable and 
decrypts its value.
+* When *Resolve argument as a variable name* is *disabled*, the text after the 
colon is treated as a *literal* encoded password and decrypted as-is.
+* *Fail if variable is not defined* (disabled by default): only applies when 
resolving a variable name. When enabled, an undefined variable causes an error 
so the pipeline or workflow fails. When disabled, a missing variable leaves the 
original expression unchanged.
+
+TIP: Enable *Fail if variable is not defined* in production environments where 
a missing password variable should stop the run rather than silently leave 
`#{hop-pwd:…}` unresolved.
+
+== Expression format
+
+Create a Variable Resolver metadata entry (for example named `hop-pwd`) of 
type *Hop Password Variable Resolver*.
+
+With *Resolve argument as a variable name* enabled (cleaner expressions):
+
+[source]
+----
+{openvar}hop-pwd:SFTP_PASSWORD{closevar}
+----
+
+If variable `SFTP_PASSWORD` holds `Encrypted 2be98afc...` (or an `AES2 ...` 
value when AES2 is configured), the expression resolves to the plaintext 
password.
+
+With the option disabled (literal ciphertext in the expression):
+
+[source]
+----
+{openvar}hop-pwd:Encrypted 2be98afc86aa7f2e4cb79a369ce93bcc9{closevar}
+----
+
+Prefer the variable-name form so pipelines do not embed ciphertext.
+
+== SFTP / VFS example
+
+Store the encoded password in a project or environment variable, for example:
+
+[source,properties]
+----
+SFTP_USER=myuser
+SFTP_PASSWORD=Encrypted 2be98afc86aa7f2e4cb79a369ce93bcc9
+SFTP_HOST=myserver
+----
+
+Then use a URI such as:
+
+[source]
+----
+sftp://${SFTP_USER}:{openvar}hop-pwd:SFTP_PASSWORD{closevar}@${SFTP_HOST}/path/to/file
+----
+
+After resolution, VFS receives a normal URI with a plaintext password in 
userinfo.
+Avoid logging resolved URIs at Detailed/Debug level if they may contain 
credentials.
+
+== Security notes
+
+* Anyone who can author pipelines and configure variable resolvers is already 
a trusted operator in Hop's threat model.
+* Decrypting default `Encrypted ...` values was always possible offline; this 
resolver only makes intentional use practical.
+* With AES2, decryption still requires `HOP_AES_ENCODER_KEY` on the runtime; 
without the key, resolution fails the same way as other password fields.
+* Prefer secrets managers when you can keep credentials out of Hop project 
files entirely.
diff --git 
a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/variable-resolver/index.adoc
 
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/variable-resolver/index.adoc
index c758199e3e..8a529cd2fd 100644
--- 
a/docs/hop-user-manual/modules/ROOT/pages/metadata-types/variable-resolver/index.adoc
+++ 
b/docs/hop-user-manual/modules/ROOT/pages/metadata-types/variable-resolver/index.adoc
@@ -59,5 +59,6 @@ Here are the available variable resolver plugins:
 * 
xref:metadata-types/variable-resolver/azure-key-vault-variable-resolver.adoc[Azure
 Key Vault variable resolver]
 * 
xref:metadata-types/variable-resolver/google-secret-manager-variable-resolver.adoc[Google
 Secret Manager Variable Resolver]
 * 
xref:metadata-types/variable-resolver/hashicorp-vault-variable-resolver.adoc[Hashicorp
 Vault variable resolver]
+* 
xref:metadata-types/variable-resolver/hop-password-variable-resolver.adoc[Hop 
Password variable resolver]
 * xref:metadata-types/variable-resolver/openbao-variable-resolver.adoc[OpenBAO 
variable resolver]
 * 
xref:metadata-types/variable-resolver/pipeline-variable-resolver.adoc[Pipeline 
variable resolver]
diff --git a/integration-tests/resolver/0001-pipeline-get-configurations.hpl 
b/integration-tests/resolver/0001-pipeline-get-configurations.hpl
new file mode 100644
index 0000000000..10653acfff
--- /dev/null
+++ b/integration-tests/resolver/0001-pipeline-get-configurations.hpl
@@ -0,0 +1,200 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+  <info>
+    <name>0001-pipeline-get-configurations</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2025/01/14 17:20:35.075</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2025/01/14 17:20:35.075</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>files/${TYPE}.properties</from>
+      <to>get values</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>get values</from>
+      <to>OUTPUT</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>OUTPUT</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>432</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>files/${TYPE}.properties</name>
+    <type>PropertyInput</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <encoding>UTF-8</encoding>
+    <fields>
+      <field>
+        <column>key</column>
+        <length>-1</length>
+        <name>Key</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <trim_type>none</trim_type>
+        <type>String</type>
+      </field>
+      <field>
+        <column>value</column>
+        <length>-1</length>
+        <name>Value</name>
+        <precision>-1</precision>
+        <repeat>N</repeat>
+        <trim_type>none</trim_type>
+        <type>String</type>
+      </field>
+    </fields>
+    <file>
+      <file_required>N</file_required>
+      <include_subfolders>N</include_subfolders>
+      <name>${PROJECT_HOME}/files/${TYPE}.properties</name>
+    </file>
+    <file_type>property</file_type>
+    <filefield>N</filefield>
+    <include>N</include>
+    <ini_section>N</ini_section>
+    <isaddresult>Y</isaddresult>
+    <limit>0</limit>
+    <resetrownumber>N</resetrownumber>
+    <resolvevaluevariable>N</resolvevaluevariable>
+    <rownum>N</rownum>
+    <attributes/>
+    <GUI>
+      <xloc>128</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>get values</name>
+    <type>Denormaliser</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <field_name>Value</field_name>
+        <key_value>hostname</key_value>
+        <target_aggregation_type>-</target_aggregation_type>
+        <target_length>-1</target_length>
+        <target_name>hostname</target_name>
+        <target_precision>-1</target_precision>
+        <target_type>String</target_type>
+      </field>
+      <field>
+        <field_name>Value</field_name>
+        <key_value>port</key_value>
+        <target_aggregation_type>-</target_aggregation_type>
+        <target_length>-1</target_length>
+        <target_name>port</target_name>
+        <target_precision>-1</target_precision>
+        <target_type>String</target_type>
+      </field>
+      <field>
+        <field_name>Value</field_name>
+        <key_value>db</key_value>
+        <target_aggregation_type>-</target_aggregation_type>
+        <target_length>-1</target_length>
+        <target_name>db</target_name>
+        <target_precision>-1</target_precision>
+        <target_type>String</target_type>
+      </field>
+      <field>
+        <field_name>Value</field_name>
+        <key_value>username</key_value>
+        <target_aggregation_type>-</target_aggregation_type>
+        <target_length>-1</target_length>
+        <target_name>username</target_name>
+        <target_precision>-1</target_precision>
+        <target_type>String</target_type>
+      </field>
+      <field>
+        <field_name>Value</field_name>
+        <key_value>password</key_value>
+        <target_aggregation_type>-</target_aggregation_type>
+        <target_length>-1</target_length>
+        <target_name>password</target_name>
+        <target_precision>-1</target_precision>
+        <target_type>String</target_type>
+      </field>
+      <field>
+        <field_name>Value</field_name>
+        <key_value>variable</key_value>
+        <target_aggregation_type>-</target_aggregation_type>
+        <target_length>-1</target_length>
+        <target_name>variable</target_name>
+        <target_precision>-1</target_precision>
+        <target_type>String</target_type>
+      </field>
+    </fields>
+    <group>
+</group>
+    <key_field>Key</key_field>
+    <attributes/>
+    <GUI>
+      <xloc>288</xloc>
+      <yloc>80</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/resolver/0001-pipeline-resolver-test.hpl 
b/integration-tests/resolver/0001-pipeline-resolver-test.hpl
new file mode 100644
index 0000000000..9874637537
--- /dev/null
+++ b/integration-tests/resolver/0001-pipeline-resolver-test.hpl
@@ -0,0 +1,705 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+  <info>
+    <name>0001-pipeline-resolver-test</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description/>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2025/01/14 17:22:13.379</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2025/01/14 17:22:13.379</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>db1</from>
+      <to>Union</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>db2</from>
+      <to>Union</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>db3</from>
+      <to>Union</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>Union</from>
+      <to>Validate</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>get db1 configuration</from>
+      <to>db1 string substitution test</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>db1 string substitution test</from>
+      <to>db1</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>get db2 configuration</from>
+      <to>db2 string substitution test</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>db2 string substitution test</from>
+      <to>db2</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>get db3 configuration</from>
+      <to>db3 string substitution test</to>
+      <enabled>Y</enabled>
+    </hop>
+    <hop>
+      <from>db3 string substitution test</from>
+      <to>db3</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Union</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>816</xloc>
+      <yloc>128</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Validate</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>928</xloc>
+      <yloc>128</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>db1</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>688</xloc>
+      <yloc>64</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>db1 string substitution test</name>
+    <type>Calculator</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <calculation>
+      <calc_type>CONSTANT</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>Hostname: #{conf:db1:hostname}; Port: #{conf:db1:port}</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>hostname_constant</field_name>
+      <grouping_symbol/>
+      <remove>Y</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <calculation>
+      <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>hostname_constant</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>hostname_str</field_name>
+      <grouping_symbol/>
+      <remove>N</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <calculation>
+      <calc_type>CONSTANT</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>{ "json": [ #{conf:db1} ] }</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>json_constant</field_name>
+      <grouping_symbol/>
+      <remove>Y</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <calculation>
+      <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>json_constant</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>json_str</field_name>
+      <grouping_symbol/>
+      <remove>N</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <calculation>
+      <calc_type>CONSTANT</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>#{conf:db1:hostname}</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>hostname_2_constant</field_name>
+      <grouping_symbol/>
+      <remove>Y</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <calculation>
+      <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>hostname_2_constant</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>hostname_2</field_name>
+      <grouping_symbol/>
+      <remove>N</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <failIfNoFile>Y</failIfNoFile>
+    <attributes/>
+    <GUI>
+      <xloc>400</xloc>
+      <yloc>64</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>db2</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>688</xloc>
+      <yloc>128</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>db2 string substitution test</name>
+    <type>Calculator</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <calculation>
+      <calc_type>CONSTANT</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>Hostname: #{conf:db2:hostname}; Port: #{conf:db2:port}</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>hostname_constant</field_name>
+      <grouping_symbol/>
+      <remove>Y</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <calculation>
+      <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>hostname_constant</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>hostname_str</field_name>
+      <grouping_symbol/>
+      <remove>N</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <calculation>
+      <calc_type>CONSTANT</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>{ "json": [ #{conf:db2} ] }</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>json_constant</field_name>
+      <grouping_symbol/>
+      <remove>Y</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <calculation>
+      <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>json_constant</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>json_str</field_name>
+      <grouping_symbol/>
+      <remove>N</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <calculation>
+      <calc_type>CONSTANT</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>#{conf:db2:hostname}</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>hostname_2_constant</field_name>
+      <grouping_symbol/>
+      <remove>Y</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <calculation>
+      <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>hostname_2_constant</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>hostname_2</field_name>
+      <grouping_symbol/>
+      <remove>N</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <failIfNoFile>Y</failIfNoFile>
+    <attributes/>
+    <GUI>
+      <xloc>400</xloc>
+      <yloc>128</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>db3</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>688</xloc>
+      <yloc>192</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>db3 string substitution test</name>
+    <type>Calculator</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <calculation>
+      <calc_type>CONSTANT</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>Hostname: #{conf:db3:hostname}; Port: #{conf:db3:port}</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>hostname_constant</field_name>
+      <grouping_symbol/>
+      <remove>Y</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <calculation>
+      <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>hostname_constant</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>hostname_str</field_name>
+      <grouping_symbol/>
+      <remove>N</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <calculation>
+      <calc_type>CONSTANT</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>{ "json": [ #{conf:db3} ] }</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>json_constant</field_name>
+      <grouping_symbol/>
+      <remove>Y</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <calculation>
+      <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>json_constant</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>json_str</field_name>
+      <grouping_symbol/>
+      <remove>N</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <calculation>
+      <calc_type>CONSTANT</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>#{conf:db3:hostname}</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>hostname_2_constant</field_name>
+      <grouping_symbol/>
+      <remove>Y</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <calculation>
+      <calc_type>SUBSTITUTE_VARIABLE</calc_type>
+      <conversion_mask/>
+      <currency_symbol/>
+      <decimal_symbol/>
+      <field_a>hostname_2_constant</field_a>
+      <field_b/>
+      <field_c/>
+      <field_name>hostname_2</field_name>
+      <grouping_symbol/>
+      <remove>N</remove>
+      <value_length>-1</value_length>
+      <value_precision>-1</value_precision>
+      <value_type>String</value_type>
+    </calculation>
+    <failIfNoFile>Y</failIfNoFile>
+    <attributes/>
+    <GUI>
+      <xloc>400</xloc>
+      <yloc>192</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>get db1 configuration</name>
+    <type>GetVariable</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <length>-1</length>
+        <name>hostname</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db1:hostname}</variable>
+      </field>
+      <field>
+        <length>-1</length>
+        <name>port</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db1:port}</variable>
+      </field>
+      <field>
+        <length>-1</length>
+        <name>db</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db1:db}</variable>
+      </field>
+      <field>
+        <length>-1</length>
+        <name>username</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db1:username}</variable>
+      </field>
+      <field>
+        <length>-1</length>
+        <name>password</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db1:port}</variable>
+      </field>
+      <field>
+        <length>-1</length>
+        <name>variable</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db1:variable}</variable>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>144</xloc>
+      <yloc>64</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>get db2 configuration</name>
+    <type>GetVariable</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <length>-1</length>
+        <name>hostname</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db2:hostname}</variable>
+      </field>
+      <field>
+        <length>-1</length>
+        <name>port</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db2:port}</variable>
+      </field>
+      <field>
+        <length>-1</length>
+        <name>db</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db2:db}</variable>
+      </field>
+      <field>
+        <length>-1</length>
+        <name>username</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db2:username}</variable>
+      </field>
+      <field>
+        <length>-1</length>
+        <name>password</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db2:port}</variable>
+      </field>
+      <field>
+        <length>-1</length>
+        <name>variable</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db2:variable}</variable>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>144</xloc>
+      <yloc>128</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>get db3 configuration</name>
+    <type>GetVariable</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <length>-1</length>
+        <name>hostname</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db3:hostname}</variable>
+      </field>
+      <field>
+        <length>-1</length>
+        <name>port</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db3:port}</variable>
+      </field>
+      <field>
+        <length>-1</length>
+        <name>db</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db3:db}</variable>
+      </field>
+      <field>
+        <length>-1</length>
+        <name>username</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db3:username}</variable>
+      </field>
+      <field>
+        <length>-1</length>
+        <name>password</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db3:port}</variable>
+      </field>
+      <field>
+        <length>-1</length>
+        <name>variable</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{conf:db3:variable}</variable>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>144</xloc>
+      <yloc>192</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/resolver/0002-hop-password-variable.hpl 
b/integration-tests/resolver/0002-hop-password-variable.hpl
new file mode 100644
index 0000000000..75c3e68324
--- /dev/null
+++ b/integration-tests/resolver/0002-hop-password-variable.hpl
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+  <info>
+    <name>0002-hop-password-variable</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description>Resolve a Hop-encoded password via variable name 
(resolveAsVariable=true)</description>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/07/11 00:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/07/11 00:00:00.000</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>Get variables</from>
+      <to>Output</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Get variables</name>
+    <type>GetVariable</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <length>-1</length>
+        <name>password</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{hop-pwd:HOP_PWD}</variable>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>64</xloc>
+      <yloc>48</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Output</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>288</xloc>
+      <yloc>48</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/resolver/0003-hop-password-literal.hpl 
b/integration-tests/resolver/0003-hop-password-literal.hpl
new file mode 100644
index 0000000000..5f164615f7
--- /dev/null
+++ b/integration-tests/resolver/0003-hop-password-literal.hpl
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+  <info>
+    <name>0003-hop-password-literal</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description>Resolve a Hop-encoded password as a literal 
(resolveAsVariable=false)</description>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/07/11 00:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/07/11 00:00:00.000</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>Get variables</from>
+      <to>Output</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Get variables</name>
+    <type>GetVariable</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <length>-1</length>
+        <name>password</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{hop-pwd-literal:Encrypted 
696e7467d9eacebc03c89cc9bf1cbd649382b8de}</variable>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>64</xloc>
+      <yloc>48</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Output</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>288</xloc>
+      <yloc>48</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git a/integration-tests/resolver/0004-hop-password-missing-variable.hpl 
b/integration-tests/resolver/0004-hop-password-missing-variable.hpl
new file mode 100644
index 0000000000..40a2d53817
--- /dev/null
+++ b/integration-tests/resolver/0004-hop-password-missing-variable.hpl
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<pipeline>
+  <info>
+    <name>0004-hop-password-missing-variable</name>
+    <name_sync_with_filename>Y</name_sync_with_filename>
+    <description>Expected to fail: hop-pwd-strict with 
failIfVariableNotDefined and missing variable</description>
+    <extended_description/>
+    <pipeline_version/>
+    <pipeline_type>Normal</pipeline_type>
+    <parameters>
+    </parameters>
+    <capture_transform_performance>N</capture_transform_performance>
+    
<transform_performance_capturing_delay>1000</transform_performance_capturing_delay>
+    
<transform_performance_capturing_size_limit>100</transform_performance_capturing_size_limit>
+    <created_user>-</created_user>
+    <created_date>2026/07/11 00:00:00.000</created_date>
+    <modified_user>-</modified_user>
+    <modified_date>2026/07/11 00:00:00.000</modified_date>
+  </info>
+  <notepads>
+  </notepads>
+  <order>
+    <hop>
+      <from>Get variables</from>
+      <to>Output</to>
+      <enabled>Y</enabled>
+    </hop>
+  </order>
+  <transform>
+    <name>Get variables</name>
+    <type>GetVariable</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <fields>
+      <field>
+        <length>-1</length>
+        <name>password</name>
+        <precision>-1</precision>
+        <trim_type>none</trim_type>
+        <type>String</type>
+        <variable>#{hop-pwd-strict:DOES_NOT_EXIST}</variable>
+      </field>
+    </fields>
+    <attributes/>
+    <GUI>
+      <xloc>64</xloc>
+      <yloc>48</yloc>
+    </GUI>
+  </transform>
+  <transform>
+    <name>Output</name>
+    <type>Dummy</type>
+    <description/>
+    <distribute>Y</distribute>
+    <custom_distribution/>
+    <copies>1</copies>
+    <partitioning>
+      <method>none</method>
+      <schema_name/>
+    </partitioning>
+    <attributes/>
+    <GUI>
+      <xloc>288</xloc>
+      <yloc>48</yloc>
+    </GUI>
+  </transform>
+  <transform_error_handling>
+  </transform_error_handling>
+  <attributes/>
+</pipeline>
diff --git 
a/integration-tests/resolver/datasets/0001-golden-pipeline-resolver.csv 
b/integration-tests/resolver/datasets/0001-golden-pipeline-resolver.csv
new file mode 100644
index 0000000000..379d2e2434
--- /dev/null
+++ b/integration-tests/resolver/datasets/0001-golden-pipeline-resolver.csv
@@ -0,0 +1,4 @@
+hostname,port,db,username,password,variable,hostname_str,json,hostname_2
+hostname1,port1,db1,user1,port1,value1,Hostname: hostname1; Port: port1,"{ 
""json"": [ 
{""hostname"":""hostname1"",""password"":""pwd1"",""port"":""port1"",""variable"":""value1"",""db"":""db1"",""username"":""user1""}
 ] }",hostname1
+hostname2,port2,db2,user2,port2,value2,Hostname: hostname2; Port: port2,"{ 
""json"": [ 
{""hostname"":""hostname2"",""password"":""pwd2"",""port"":""port2"",""variable"":""value2"",""db"":""db2"",""username"":""user2""}
 ] }",hostname2
+hostname3,port3,db3,user3,port3,value3,Hostname: hostname3; Port: port3,"{ 
""json"": [ 
{""hostname"":""hostname3"",""password"":""pwd3"",""port"":""port3"",""variable"":""value3"",""db"":""db3"",""username"":""user3""}
 ] }",hostname3
diff --git 
a/integration-tests/resolver/datasets/0002-golden-hop-password-variable.csv 
b/integration-tests/resolver/datasets/0002-golden-hop-password-variable.csv
new file mode 100644
index 0000000000..b592e0a8a1
--- /dev/null
+++ b/integration-tests/resolver/datasets/0002-golden-hop-password-variable.csv
@@ -0,0 +1,2 @@
+password
+integration-test-pwd
diff --git 
a/integration-tests/resolver/datasets/0003-golden-hop-password-literal.csv 
b/integration-tests/resolver/datasets/0003-golden-hop-password-literal.csv
new file mode 100644
index 0000000000..b592e0a8a1
--- /dev/null
+++ b/integration-tests/resolver/datasets/0003-golden-hop-password-literal.csv
@@ -0,0 +1,2 @@
+password
+integration-test-pwd
diff --git a/integration-tests/resolver/dev-env-config.json 
b/integration-tests/resolver/dev-env-config.json
new file mode 100644
index 0000000000..e5833d836c
--- /dev/null
+++ b/integration-tests/resolver/dev-env-config.json
@@ -0,0 +1,11 @@
+{
+  "variables" : [ {
+    "name" : "HOP_PWD",
+    "value" : "Encrypted 696e7467d9eacebc03c89cc9bf1cbd649382b8de",
+    "description" : "Hop-encoded password for plaintext integration-test-pwd"
+  }, {
+    "name" : "HOP_PWD_PLAIN",
+    "value" : "integration-test-pwd",
+    "description" : "Expected plaintext after decrypt"
+  } ]
+}
diff --git a/integration-tests/resolver/files/db1.properties 
b/integration-tests/resolver/files/db1.properties
new file mode 100644
index 0000000000..9d0d3cd862
--- /dev/null
+++ b/integration-tests/resolver/files/db1.properties
@@ -0,0 +1,23 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#
+username=user1
+password=pwd1
+hostname=hostname1
+port=port1
+db=db1
+variable=value1
diff --git a/integration-tests/resolver/files/db2.properties 
b/integration-tests/resolver/files/db2.properties
new file mode 100644
index 0000000000..18e42138fa
--- /dev/null
+++ b/integration-tests/resolver/files/db2.properties
@@ -0,0 +1,23 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#
+username=user2
+password=pwd2
+hostname=hostname2
+port=port2
+db=db2
+variable=value2
diff --git a/integration-tests/resolver/files/db3.properties 
b/integration-tests/resolver/files/db3.properties
new file mode 100644
index 0000000000..4f5974c77d
--- /dev/null
+++ b/integration-tests/resolver/files/db3.properties
@@ -0,0 +1,24 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#
+username=user3
+password=pwd3
+hostname=hostname3
+port=port3
+db=db3
+variable=value3
+
diff --git a/integration-tests/resolver/hop-config.json 
b/integration-tests/resolver/hop-config.json
new file mode 100644
index 0000000000..102ac981d5
--- /dev/null
+++ b/integration-tests/resolver/hop-config.json
@@ -0,0 +1,290 @@
+{
+  "variables": [
+    {
+      "name": "HOP_LENIENT_STRING_TO_NUMBER_CONVERSION",
+      "value": "N",
+      "description": "System wide flag to allow lenient string to number 
conversion for backward compatibility. If this setting is set to \"Y\", an 
string starting with digits will be converted successfully into a number. 
(example: 192.168.1.1 will be converted into 192 or 192.168 or 192168 depending 
on the decimal and grouping symbol). The default (N) will be to throw an error 
if non-numeric symbols are found in the string."
+    },
+    {
+      "name": "HOP_COMPATIBILITY_DB_IGNORE_TIMEZONE",
+      "value": "N",
+      "description": "System wide flag to ignore timezone while writing 
date/timestamp value to the database."
+    },
+    {
+      "name": "HOP_LOG_SIZE_LIMIT",
+      "value": "0",
+      "description": "The log size limit for all pipelines and workflows that 
don't have the \"log size limit\" property set in their respective properties."
+    },
+    {
+      "name": "HOP_EMPTY_STRING_DIFFERS_FROM_NULL",
+      "value": "N",
+      "description": "NULL vs Empty String. If this setting is set to Y, an 
empty string and null are different. Otherwise they are not."
+    },
+    {
+      "name": "HOP_MAX_LOG_SIZE_IN_LINES",
+      "value": "0",
+      "description": "The maximum number of log lines that are kept internally 
by Hop. Set to 0 to keep all rows (default)"
+    },
+    {
+      "name": "HOP_MAX_LOG_TIMEOUT_IN_MINUTES",
+      "value": "1440",
+      "description": "The maximum age (in minutes) of a log line while being 
kept internally by Hop. Set to 0 to keep all rows indefinitely (default)"
+    },
+    {
+      "name": "HOP_MAX_WORKFLOW_TRACKER_SIZE",
+      "value": "5000",
+      "description": "The maximum number of workflow trackers kept in memory"
+    },
+    {
+      "name": "HOP_MAX_ACTIONS_LOGGED",
+      "value": "5000",
+      "description": "The maximum number of action results kept in memory for 
logging purposes."
+    },
+    {
+      "name": "HOP_MAX_LOGGING_REGISTRY_SIZE",
+      "value": "10000",
+      "description": "The maximum number of logging registry entries kept in 
memory for logging purposes."
+    },
+    {
+      "name": "HOP_LOG_TAB_REFRESH_DELAY",
+      "value": "1000",
+      "description": "The hop log tab refresh delay."
+    },
+    {
+      "name": "HOP_LOG_TAB_REFRESH_PERIOD",
+      "value": "1000",
+      "description": "The hop log tab refresh period."
+    },
+    {
+      "name": "HOP_PLUGIN_CLASSES",
+      "value": null,
+      "description": "A comma delimited list of classes to scan for plugin 
annotations"
+    },
+    {
+      "name": "HOP_PLUGIN_PACKAGES",
+      "value": null,
+      "description": "A comma delimited list of packages to scan for plugin 
annotations (warning: slow!!)"
+    },
+    {
+      "name": "HOP_TRANSFORM_PERFORMANCE_SNAPSHOT_LIMIT",
+      "value": "0",
+      "description": "The maximum number of transform performance snapshots to 
keep in memory. Set to 0 to keep all snapshots indefinitely (default)"
+    },
+    {
+      "name": "HOP_ROWSET_GET_TIMEOUT",
+      "value": "50",
+      "description": "The name of the variable that optionally contains an 
alternative rowset get timeout (in ms). This only makes a difference for 
extremely short lived pipelines."
+    },
+    {
+      "name": "HOP_ROWSET_PUT_TIMEOUT",
+      "value": "50",
+      "description": "The name of the variable that optionally contains an 
alternative rowset put timeout (in ms). This only makes a difference for 
extremely short lived pipelines."
+    },
+    {
+      "name": "HOP_CORE_TRANSFORMS_FILE",
+      "value": null,
+      "description": "The name of the project variable that will contain the 
alternative location of the hop-transforms.xml file. You can use this to 
customize the list of available internal transforms outside of the codebase."
+    },
+    {
+      "name": "HOP_CORE_WORKFLOW_ACTIONS_FILE",
+      "value": null,
+      "description": "The name of the project variable that will contain the 
alternative location of the hop-workflow-actions.xml file."
+    },
+    {
+      "name": "HOP_SERVER_OBJECT_TIMEOUT_MINUTES",
+      "value": "1440",
+      "description": "This project variable will set a time-out after which 
waiting, completed or stopped pipelines and workflows will be automatically 
cleaned up. The default value is 1440 (one day)."
+    },
+    {
+      "name": "HOP_PIPELINE_PAN_JVM_EXIT_CODE",
+      "value": null,
+      "description": "Set this variable to an integer that will be returned as 
the Pan JVM exit code."
+    },
+    {
+      "name": "HOP_DISABLE_CONSOLE_LOGGING",
+      "value": "N",
+      "description": "Set this variable to Y to disable standard Hop logging 
to the console. (stdout)"
+    },
+    {
+      "name": "HOP_REDIRECT_STDERR",
+      "value": "N",
+      "description": "Set this variable to Y to redirect stderr to Hop 
logging."
+    },
+    {
+      "name": "HOP_REDIRECT_STDOUT",
+      "value": "N",
+      "description": "Set this variable to Y to redirect stdout to Hop 
logging."
+    },
+    {
+      "name": "HOP_DEFAULT_NUMBER_FORMAT",
+      "value": null,
+      "description": "The name of the variable containing an alternative 
default number format"
+    },
+    {
+      "name": "HOP_DEFAULT_BIGNUMBER_FORMAT",
+      "value": null,
+      "description": "The name of the variable containing an alternative 
default bignumber format"
+    },
+    {
+      "name": "HOP_DEFAULT_INTEGER_FORMAT",
+      "value": null,
+      "description": "The name of the variable containing an alternative 
default integer format"
+    },
+    {
+      "name": "HOP_DEFAULT_DATE_FORMAT",
+      "value": null,
+      "description": "The name of the variable containing an alternative 
default date format"
+    },
+    {
+      "name": "HOP_DEFAULT_TIMESTAMP_FORMAT",
+      "value": null,
+      "description": "The name of the variable containing an alternative 
default timestamp format"
+    },
+    {
+      "name": "HOP_DEFAULT_SERVLET_ENCODING",
+      "value": null,
+      "description": "Defines the default encoding for servlets, leave it 
empty to use Java default encoding"
+    },
+    {
+      "name": "HOP_FAIL_ON_LOGGING_ERROR",
+      "value": "N",
+      "description": "Set this variable to Y when you want the 
workflow/pipeline fail with an error when the related logging process (e.g. to 
a database) fails."
+    },
+    {
+      "name": "HOP_AGGREGATION_MIN_NULL_IS_VALUED",
+      "value": "N",
+      "description": "Set this variable to Y to set the minimum to NULL if 
NULL is within an aggregate. Otherwise by default NULL is ignored by the MIN 
aggregate and MIN is set to the minimum value that is not NULL. See also the 
variable HOP_AGGREGATION_ALL_NULLS_ARE_ZERO."
+    },
+    {
+      "name": "HOP_AGGREGATION_ALL_NULLS_ARE_ZERO",
+      "value": "N",
+      "description": "Set this variable to Y to return 0 when all values 
within an aggregate are NULL. Otherwise by default a NULL is returned when all 
values are NULL."
+    },
+    {
+      "name": "HOP_COMPATIBILITY_TEXT_FILE_OUTPUT_APPEND_NO_HEADER",
+      "value": "N",
+      "description": "Set this variable to Y for backward compatibility for 
the Text File Output transform. Setting this to Ywill add no header row at all 
when the append option is enabled, regardless if the file is existing or not."
+    },
+    {
+      "name": "HOP_PASSWORD_ENCODER_PLUGIN",
+      "value": "Hop",
+      "description": "Specifies the password encoder plugin to use by ID (Hop 
is the default)."
+    },
+    {
+      "name": "HOP_SYSTEM_HOSTNAME",
+      "value": null,
+      "description": "You can use this variable to speed up hostname lookup. 
Hostname lookup is performed by Hop so that it is capable of logging the server 
on which a workflow or pipeline is executed."
+    },
+    {
+      "name": "HOP_SERVER_JETTY_ACCEPTORS",
+      "value": null,
+      "description": "A variable to configure jetty option: acceptors for 
Carte"
+    },
+    {
+      "name": "HOP_SERVER_JETTY_ACCEPT_QUEUE_SIZE",
+      "value": null,
+      "description": "A variable to configure jetty option: acceptQueueSize 
for Carte"
+    },
+    {
+      "name": "HOP_SERVER_JETTY_RES_MAX_IDLE_TIME",
+      "value": null,
+      "description": "A variable to configure jetty option: 
lowResourcesMaxIdleTime for Carte"
+    },
+    {
+      "name": 
"HOP_COMPATIBILITY_MERGE_ROWS_USE_REFERENCE_STREAM_WHEN_IDENTICAL",
+      "value": "N",
+      "description": "Set this variable to Y for backward compatibility for 
the Merge Rows (diff) transform. Setting this to Y will use the data from the 
reference stream (instead of the comparison stream) in case the compared rows 
are identical."
+    },
+    {
+      "name": "HOP_SPLIT_FIELDS_REMOVE_ENCLOSURE",
+      "value": "false",
+      "description": "Set this variable to false to preserve enclosure symbol 
after splitting the string in the Split fields transform. Changing it to true 
will remove first and last enclosure symbol from the resulting string chunks."
+    },
+    {
+      "name": "HOP_ALLOW_EMPTY_FIELD_NAMES_AND_TYPES",
+      "value": "false",
+      "description": "Set this variable to TRUE to allow your pipeline to pass 
'null' fields and/or empty types."
+    },
+    {
+      "name": "HOP_GLOBAL_LOG_VARIABLES_CLEAR_ON_EXPORT",
+      "value": "false",
+      "description": "Set this variable to false to preserve global log 
variables defined in pipeline / workflow Properties -> Log panel. Changing it 
to true will clear it when export pipeline / workflow."
+    },
+    {
+      "name": "HOP_FILE_OUTPUT_MAX_STREAM_COUNT",
+      "value": "1024",
+      "description": "This project variable is used by the Text File Output 
transform. It defines the max number of simultaneously open files within the 
transform. The transform will close/reopen files as necessary to insure the max 
is not exceeded"
+    },
+    {
+      "name": "HOP_FILE_OUTPUT_MAX_STREAM_LIFE",
+      "value": "0",
+      "description": "This project variable is used by the Text File Output 
transform. It defines the max number of milliseconds between flushes of files 
opened by the transform."
+    },
+    {
+      "name": "HOP_USE_NATIVE_FILE_DIALOG",
+      "value": "N",
+      "description": "Set this value to Y if you want to use the system file 
open/save dialog when browsing files"
+    },
+    {
+      "name": "HOP_AUTO_CREATE_CONFIG",
+      "value": "Y",
+      "description": "Set this value to N if you don't want to automatically 
create a hop configuration file (hop-config.json) when it's missing"
+    }
+  ],
+  "LocaleDefault": "en_BE",
+  "guiProperties": {
+    "FontFixedSize": "13",
+    "MaxUndo": "100",
+    "DarkMode": "Y",
+    "FontNoteSize": "13",
+    "ShowOSLook": "Y",
+    "FontFixedStyle": "0",
+    "FontNoteName": ".AppleSystemUIFont",
+    "FontFixedName": "Monospaced",
+    "FontGraphStyle": "0",
+    "FontDefaultSize": "13",
+    "GraphColorR": "255",
+    "FontGraphSize": "13",
+    "IconSize": "32",
+    "BackgroundColorB": "255",
+    "FontNoteStyle": "0",
+    "FontGraphName": ".AppleSystemUIFont",
+    "FontDefaultName": ".AppleSystemUIFont",
+    "GraphColorG": "255",
+    "UseGlobalFileBookmarks": "Y",
+    "FontDefaultStyle": "0",
+    "GraphColorB": "255",
+    "BackgroundColorR": "255",
+    "BackgroundColorG": "255",
+    "WorkflowDialogStyle": "RESIZE,MAX,MIN",
+    "LineWidth": "1",
+    "ContextDialogShowCategories": "Y"
+  },
+  "projectsConfig": {
+    "enabled": true,
+    "projectMandatory": true,
+    "environmentMandatory": true,
+    "defaultProject": "default",
+    "defaultEnvironment": null,
+    "standardParentProject": "default",
+    "standardProjectsFolder": null,
+    "projectConfigurations": [
+      {
+        "projectName": "default",
+        "projectHome": "${HOP_CONFIG_FOLDER}",
+        "configFilename": "project-config.json"
+      }
+    ],
+    "lifecycleEnvironments": [
+      {
+        "name": "dev",
+        "purpose": "Testing",
+        "projectName": "default",
+        "configurationFiles": [
+          "${PROJECT_HOME}/dev-env-config.json"
+        ]
+      }
+    ],
+    "projectLifecycles": []
+  }
+}
\ No newline at end of file
diff --git a/integration-tests/resolver/main-0001-pipeline-resolver.hwf 
b/integration-tests/resolver/main-0001-pipeline-resolver.hwf
new file mode 100644
index 0000000000..3dfa918dbd
--- /dev/null
+++ b/integration-tests/resolver/main-0001-pipeline-resolver.hwf
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<workflow>
+  <name>main-0001-pipeline-resolver</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description/>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2025/01/14 17:21:30.278</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2025/01/14 17:21:30.278</modified_date>
+  <parameters>
+    </parameters>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <DayOfMonth>1</DayOfMonth>
+      <hour>12</hour>
+      <intervalMinutes>60</intervalMinutes>
+      <intervalSeconds>0</intervalSeconds>
+      <minutes>0</minutes>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <weekDay>1</weekDay>
+      <parallel>N</parallel>
+      <xloc>96</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Run Pipeline Unit Tests</name>
+      <description/>
+      <type>RunPipelineTests</type>
+      <attributes/>
+      <test_names>
+        <test_name>
+          <name>0001-pipeline-resolver-test UNIT</name>
+        </test_name>
+      </test_names>
+      <parallel>N</parallel>
+      <xloc>256</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>Run Pipeline Unit Tests</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git a/integration-tests/resolver/main-0002-hop-password-variable.hwf 
b/integration-tests/resolver/main-0002-hop-password-variable.hwf
new file mode 100644
index 0000000000..d69e79fde2
--- /dev/null
+++ b/integration-tests/resolver/main-0002-hop-password-variable.hwf
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<workflow>
+  <name>main-0002-hop-password-variable</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description/>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2026/07/11 00:00:00.000</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2026/07/11 00:00:00.000</modified_date>
+  <parameters>
+    </parameters>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <DayOfMonth>1</DayOfMonth>
+      <hour>12</hour>
+      <intervalMinutes>60</intervalMinutes>
+      <intervalSeconds>0</intervalSeconds>
+      <minutes>0</minutes>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <weekDay>1</weekDay>
+      <parallel>N</parallel>
+      <xloc>96</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Run Pipeline Unit Tests</name>
+      <description/>
+      <type>RunPipelineTests</type>
+      <attributes/>
+      <test_names>
+        <test_name>
+          <name>0002-hop-password-variable UNIT</name>
+        </test_name>
+      </test_names>
+      <parallel>N</parallel>
+      <xloc>256</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>Run Pipeline Unit Tests</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git a/integration-tests/resolver/main-0003-hop-password-literal.hwf 
b/integration-tests/resolver/main-0003-hop-password-literal.hwf
new file mode 100644
index 0000000000..b1d4d39414
--- /dev/null
+++ b/integration-tests/resolver/main-0003-hop-password-literal.hwf
@@ -0,0 +1,79 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<workflow>
+  <name>main-0003-hop-password-literal</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description/>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2026/07/11 00:00:00.000</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2026/07/11 00:00:00.000</modified_date>
+  <parameters>
+    </parameters>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <DayOfMonth>1</DayOfMonth>
+      <hour>12</hour>
+      <intervalMinutes>60</intervalMinutes>
+      <intervalSeconds>0</intervalSeconds>
+      <minutes>0</minutes>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <weekDay>1</weekDay>
+      <parallel>N</parallel>
+      <xloc>96</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Run Pipeline Unit Tests</name>
+      <description/>
+      <type>RunPipelineTests</type>
+      <attributes/>
+      <test_names>
+        <test_name>
+          <name>0003-hop-password-literal UNIT</name>
+        </test_name>
+      </test_names>
+      <parallel>N</parallel>
+      <xloc>256</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>Run Pipeline Unit Tests</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git 
a/integration-tests/resolver/main-0004-hop-password-missing-variable.hwf 
b/integration-tests/resolver/main-0004-hop-password-missing-variable.hwf
new file mode 100644
index 0000000000..ded4e91171
--- /dev/null
+++ b/integration-tests/resolver/main-0004-hop-password-missing-variable.hwf
@@ -0,0 +1,146 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+
+Licensed to the Apache Software Foundation (ASF) under one or more
+contributor license agreements.  See the NOTICE file distributed with
+this work for additional information regarding copyright ownership.
+The ASF licenses this file to You under the Apache License, Version 2.0
+(the "License"); you may not use this file except in compliance with
+the License.  You may obtain a copy of the License at
+
+      http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+-->
+<workflow>
+  <name>main-0004-hop-password-missing-variable</name>
+  <name_sync_with_filename>Y</name_sync_with_filename>
+  <description>Expect pipeline to fail when hop-pwd-strict cannot resolve a 
missing variable</description>
+  <extended_description/>
+  <workflow_version/>
+  <created_user>-</created_user>
+  <created_date>2026/07/11 00:00:00.000</created_date>
+  <modified_user>-</modified_user>
+  <modified_date>2026/07/11 00:00:00.000</modified_date>
+  <parameters>
+    </parameters>
+  <actions>
+    <action>
+      <name>Start</name>
+      <description/>
+      <type>SPECIAL</type>
+      <attributes/>
+      <DayOfMonth>1</DayOfMonth>
+      <hour>12</hour>
+      <intervalMinutes>60</intervalMinutes>
+      <intervalSeconds>0</intervalSeconds>
+      <minutes>0</minutes>
+      <repeat>N</repeat>
+      <schedulerType>0</schedulerType>
+      <weekDay>1</weekDay>
+      <parallel>N</parallel>
+      <xloc>96</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>0004-hop-password-missing-variable.hpl</name>
+      <description/>
+      <type>PIPELINE</type>
+      <attributes/>
+      
<filename>${PROJECT_HOME}/0004-hop-password-missing-variable.hpl</filename>
+      <params_from_previous>N</params_from_previous>
+      <exec_per_row>N</exec_per_row>
+      <clear_rows>N</clear_rows>
+      <clear_files>N</clear_files>
+      <set_logfile>N</set_logfile>
+      <logfile/>
+      <logext/>
+      <add_date>N</add_date>
+      <add_time>N</add_time>
+      <loglevel>Basic</loglevel>
+      <set_append_logfile>N</set_append_logfile>
+      <wait_until_finished>Y</wait_until_finished>
+      <create_parent_folder>N</create_parent_folder>
+      <run_configuration>local</run_configuration>
+      <parameters>
+        <pass_all_parameters>Y</pass_all_parameters>
+      </parameters>
+      <parallel>N</parallel>
+      <xloc>336</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Check pipeline log</name>
+      <description/>
+      <type>EVAL</type>
+      <attributes/>
+      <script>
+var txt = previous_result.getLogText();
+
+var ok = true;
+
+var expectedValues = [
+   "DOES_NOT_EXIST",
+   "Password variable",
+ ];
+
+for (var i = 0 ; i&lt;expectedValues.length ; i++) {
+  var expectedValue = expectedValues[i];
+  if ( !txt.contains(expectedValue)) {
+    ok = false;
+    log.logError("Value logged as '"+expectedValue+"' was not logged at least 
once");
+  }
+}
+
+ok;</script>
+      <parallel>N</parallel>
+      <xloc>576</xloc>
+      <yloc>64</yloc>
+      <attributes_hac/>
+    </action>
+    <action>
+      <name>Fail</name>
+      <description/>
+      <type>ABORT</type>
+      <attributes/>
+      <message>This pipeline should fail when the password variable is not 
defined</message>
+      <parallel>N</parallel>
+      <xloc>576</xloc>
+      <yloc>176</yloc>
+      <attributes_hac/>
+    </action>
+  </actions>
+  <hops>
+    <hop>
+      <from>Start</from>
+      <to>0004-hop-password-missing-variable.hpl</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>Y</unconditional>
+    </hop>
+    <hop>
+      <from>0004-hop-password-missing-variable.hpl</from>
+      <to>Check pipeline log</to>
+      <enabled>Y</enabled>
+      <evaluation>N</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+    <hop>
+      <from>0004-hop-password-missing-variable.hpl</from>
+      <to>Fail</to>
+      <enabled>Y</enabled>
+      <evaluation>Y</evaluation>
+      <unconditional>N</unconditional>
+    </hop>
+  </hops>
+  <notepads>
+  </notepads>
+  <attributes/>
+</workflow>
diff --git 
a/integration-tests/resolver/metadata/dataset/0001-golden-pipeline-resolver.json
 
b/integration-tests/resolver/metadata/dataset/0001-golden-pipeline-resolver.json
new file mode 100644
index 0000000000..a96760571b
--- /dev/null
+++ 
b/integration-tests/resolver/metadata/dataset/0001-golden-pipeline-resolver.json
@@ -0,0 +1,80 @@
+{
+  "base_filename": "0001-golden-pipeline-resolver.csv",
+  "name": "0001-golden-pipeline-resolver",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "hostname",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "port",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "db",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "username",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "password",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "variable",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "hostname_str",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "json",
+      "field_format": ""
+    },
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "hostname_2",
+      "field_format": ""
+    }
+  ],
+  "folder_name": ""
+}
diff --git 
a/integration-tests/resolver/metadata/dataset/0002-golden-hop-password-variable.json
 
b/integration-tests/resolver/metadata/dataset/0002-golden-hop-password-variable.json
new file mode 100644
index 0000000000..d0af433582
--- /dev/null
+++ 
b/integration-tests/resolver/metadata/dataset/0002-golden-hop-password-variable.json
@@ -0,0 +1,16 @@
+{
+  "base_filename": "0002-golden-hop-password-variable.csv",
+  "name": "0002-golden-hop-password-variable",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "password",
+      "field_format": ""
+    }
+  ],
+  "folder_name": ""
+}
diff --git 
a/integration-tests/resolver/metadata/dataset/0003-golden-hop-password-literal.json
 
b/integration-tests/resolver/metadata/dataset/0003-golden-hop-password-literal.json
new file mode 100644
index 0000000000..ff906dc04a
--- /dev/null
+++ 
b/integration-tests/resolver/metadata/dataset/0003-golden-hop-password-literal.json
@@ -0,0 +1,16 @@
+{
+  "base_filename": "0003-golden-hop-password-literal.csv",
+  "name": "0003-golden-hop-password-literal",
+  "description": "",
+  "dataset_fields": [
+    {
+      "field_comment": "",
+      "field_length": -1,
+      "field_type": 2,
+      "field_precision": -1,
+      "field_name": "password",
+      "field_format": ""
+    }
+  ],
+  "folder_name": ""
+}
diff --git 
a/integration-tests/resolver/metadata/pipeline-run-configuration/local.json 
b/integration-tests/resolver/metadata/pipeline-run-configuration/local.json
new file mode 100644
index 0000000000..d6b7ca4eed
--- /dev/null
+++ b/integration-tests/resolver/metadata/pipeline-run-configuration/local.json
@@ -0,0 +1,20 @@
+{
+  "engineRunConfiguration": {
+    "Local": {
+      "feedback_size": "50000",
+      "sample_size": "100",
+      "sample_type_in_gui": "Last",
+      "wait_time": "20",
+      "rowset_size": "10000",
+      "safe_mode": false,
+      "show_feedback": false,
+      "topo_sort": false,
+      "gather_metrics": false,
+      "transactional": false
+    }
+  },
+  "defaultSelection": true,
+  "configurationVariables": [],
+  "name": "local",
+  "description": "Runs your pipelines locally with the standard local Hop 
pipeline engine"
+}
\ No newline at end of file
diff --git 
a/integration-tests/resolver/metadata/unit-test/0001-pipeline-resolver-test 
UNIT.json 
b/integration-tests/resolver/metadata/unit-test/0001-pipeline-resolver-test 
UNIT.json
new file mode 100644
index 0000000000..55d1816370
--- /dev/null
+++ b/integration-tests/resolver/metadata/unit-test/0001-pipeline-resolver-test 
UNIT.json       
@@ -0,0 +1,68 @@
+{
+  "database_replacements": [],
+  "autoOpening": true,
+  "description": "",
+  "persist_filename": "",
+  "test_type": "UNIT_TEST",
+  "variableValues": [],
+  "basePath": "",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "hostname",
+          "data_set_field": "hostname"
+        },
+        {
+          "transform_field": "port",
+          "data_set_field": "port"
+        },
+        {
+          "transform_field": "db",
+          "data_set_field": "db"
+        },
+        {
+          "transform_field": "username",
+          "data_set_field": "username"
+        },
+        {
+          "transform_field": "password",
+          "data_set_field": "password"
+        },
+        {
+          "transform_field": "variable",
+          "data_set_field": "variable"
+        },
+        {
+          "transform_field": "hostname_str",
+          "data_set_field": "hostname_str"
+        },
+        {
+          "transform_field": "json_str",
+          "data_set_field": "json"
+        },
+        {
+          "transform_field": "hostname_2",
+          "data_set_field": "hostname_2"
+        }
+      ],
+      "field_order": [
+        "hostname",
+        "port",
+        "db",
+        "username",
+        "password",
+        "variable",
+        "hostname_str",
+        "json",
+        "hostname_2"
+      ],
+      "data_set_name": "0001-golden-pipeline-resolver",
+      "transform_name": "Validate"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0001-pipeline-resolver-test UNIT",
+  "trans_test_tweaks": [],
+  "pipeline_filename": "./0001-pipeline-resolver-test.hpl"
+}
\ No newline at end of file
diff --git 
a/integration-tests/resolver/metadata/unit-test/0002-hop-password-variable 
UNIT.json 
b/integration-tests/resolver/metadata/unit-test/0002-hop-password-variable 
UNIT.json
new file mode 100644
index 0000000000..9156dd85c4
--- /dev/null
+++ b/integration-tests/resolver/metadata/unit-test/0002-hop-password-variable 
UNIT.json        
@@ -0,0 +1,28 @@
+{
+  "database_replacements": [],
+  "autoOpening": true,
+  "description": "",
+  "persist_filename": "",
+  "test_type": "UNIT_TEST",
+  "variableValues": [],
+  "basePath": "",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "password",
+          "data_set_field": "password"
+        }
+      ],
+      "field_order": [
+        "password"
+      ],
+      "data_set_name": "0002-golden-hop-password-variable",
+      "transform_name": "Output"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0002-hop-password-variable UNIT",
+  "trans_test_tweaks": [],
+  "pipeline_filename": "./0002-hop-password-variable.hpl"
+}
diff --git 
a/integration-tests/resolver/metadata/unit-test/0003-hop-password-literal 
UNIT.json 
b/integration-tests/resolver/metadata/unit-test/0003-hop-password-literal 
UNIT.json
new file mode 100644
index 0000000000..feadde6bf3
--- /dev/null
+++ b/integration-tests/resolver/metadata/unit-test/0003-hop-password-literal 
UNIT.json 
@@ -0,0 +1,28 @@
+{
+  "database_replacements": [],
+  "autoOpening": true,
+  "description": "",
+  "persist_filename": "",
+  "test_type": "UNIT_TEST",
+  "variableValues": [],
+  "basePath": "",
+  "golden_data_sets": [
+    {
+      "field_mappings": [
+        {
+          "transform_field": "password",
+          "data_set_field": "password"
+        }
+      ],
+      "field_order": [
+        "password"
+      ],
+      "data_set_name": "0003-golden-hop-password-literal",
+      "transform_name": "Output"
+    }
+  ],
+  "input_data_sets": [],
+  "name": "0003-hop-password-literal UNIT",
+  "trans_test_tweaks": [],
+  "pipeline_filename": "./0003-hop-password-literal.hpl"
+}
diff --git a/integration-tests/resolver/metadata/variable-resolver/conf.json 
b/integration-tests/resolver/metadata/variable-resolver/conf.json
new file mode 100644
index 0000000000..bd95082169
--- /dev/null
+++ b/integration-tests/resolver/metadata/variable-resolver/conf.json
@@ -0,0 +1,13 @@
+{
+  "virtualPath": "",
+  "name": "conf",
+  "description": "Uses a pipeline to read configuration files using variable 
expressions",
+  "variable-resolver": {
+    "Variable-Resolver-Pipeline": {
+      "outputTransformName": "OUTPUT",
+      "filename": "${PROJECT_HOME}/0001-pipeline-get-configurations.hpl",
+      "expressionVariableName": "TYPE",
+      "runConfigurationName": "local"
+    }
+  }
+}
\ No newline at end of file
diff --git 
a/integration-tests/resolver/metadata/variable-resolver/hop-pwd-literal.json 
b/integration-tests/resolver/metadata/variable-resolver/hop-pwd-literal.json
new file mode 100644
index 0000000000..a96568e0a1
--- /dev/null
+++ b/integration-tests/resolver/metadata/variable-resolver/hop-pwd-literal.json
@@ -0,0 +1,11 @@
+{
+  "virtualPath": "",
+  "name": "hop-pwd-literal",
+  "description": "Decrypt Hop passwords; argument is literal Encrypted/AES2 
text",
+  "variable-resolver": {
+    "HopPassword": {
+      "resolveAsVariable": false,
+      "failIfVariableNotDefined": false
+    }
+  }
+}
diff --git 
a/integration-tests/resolver/metadata/variable-resolver/hop-pwd-strict.json 
b/integration-tests/resolver/metadata/variable-resolver/hop-pwd-strict.json
new file mode 100644
index 0000000000..3677cdc2a6
--- /dev/null
+++ b/integration-tests/resolver/metadata/variable-resolver/hop-pwd-strict.json
@@ -0,0 +1,11 @@
+{
+  "virtualPath": "",
+  "name": "hop-pwd-strict",
+  "description": "Decrypt Hop passwords from variable names; fail if the 
variable is not defined",
+  "variable-resolver": {
+    "HopPassword": {
+      "resolveAsVariable": true,
+      "failIfVariableNotDefined": true
+    }
+  }
+}
diff --git a/integration-tests/resolver/metadata/variable-resolver/hop-pwd.json 
b/integration-tests/resolver/metadata/variable-resolver/hop-pwd.json
new file mode 100644
index 0000000000..60dd2fda4b
--- /dev/null
+++ b/integration-tests/resolver/metadata/variable-resolver/hop-pwd.json
@@ -0,0 +1,11 @@
+{
+  "virtualPath": "",
+  "name": "hop-pwd",
+  "description": "Decrypt Hop passwords; argument is a variable name",
+  "variable-resolver": {
+    "HopPassword": {
+      "resolveAsVariable": true,
+      "failIfVariableNotDefined": false
+    }
+  }
+}
diff --git 
a/integration-tests/resolver/metadata/workflow-run-configuration/local.json 
b/integration-tests/resolver/metadata/workflow-run-configuration/local.json
new file mode 100644
index 0000000000..e37a93039a
--- /dev/null
+++ b/integration-tests/resolver/metadata/workflow-run-configuration/local.json
@@ -0,0 +1,9 @@
+{
+  "engineRunConfiguration": {
+    "Local": {
+      "safe_mode": false
+    }
+  },
+  "name": "local",
+  "description": "Runs your workflows locally with the standard local Hop 
workflow engine"
+}
\ No newline at end of file
diff --git a/integration-tests/resolver/project-config.json 
b/integration-tests/resolver/project-config.json
new file mode 100644
index 0000000000..899c6c1092
--- /dev/null
+++ b/integration-tests/resolver/project-config.json
@@ -0,0 +1,15 @@
+{
+  "metadataBaseFolder": "${PROJECT_HOME}/metadata",
+  "unitTestsBasePath": "${PROJECT_HOME}",
+  "dataSetsCsvFolder": "${PROJECT_HOME}/datasets",
+  "enforcingExecutionInHome": true,
+  "config": {
+    "variables": [
+      {
+        "name": "HOP_LICENSE_HEADER_FILE",
+        "value": "${PROJECT_HOME}/../asf-header.txt",
+        "description": "This will automatically serialize the ASF license 
header into pipelines and workflows in the integration test projects"
+      }
+    ]
+  }
+}
\ No newline at end of file
diff --git 
a/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/resolver/HopPasswordVariableResolver.java
 
b/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/resolver/HopPasswordVariableResolver.java
new file mode 100644
index 0000000000..6f628d8960
--- /dev/null
+++ 
b/plugins/misc/passwords/src/main/java/org/apache/hop/passwords/resolver/HopPasswordVariableResolver.java
@@ -0,0 +1,139 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.hop.passwords.resolver;
+
+import lombok.Getter;
+import lombok.Setter;
+import org.apache.commons.lang3.StringUtils;
+import org.apache.hop.core.encryption.Encr;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.gui.plugin.GuiElementType;
+import org.apache.hop.core.gui.plugin.GuiPlugin;
+import org.apache.hop.core.gui.plugin.GuiWidgetElement;
+import org.apache.hop.core.variables.IVariables;
+import org.apache.hop.core.variables.resolver.IVariableResolver;
+import org.apache.hop.core.variables.resolver.VariableResolver;
+import org.apache.hop.core.variables.resolver.VariableResolverPlugin;
+import org.apache.hop.metadata.api.HopMetadataProperty;
+
+/**
+ * Resolves Hop-encoded (or optionally encrypted) password values for use in 
places that do not call
+ * {@link Encr} themselves, such as VFS URIs (SFTP, etc.).
+ *
+ * <p>Expression format: {@code #{resolver-name:argument}}
+ *
+ * <ul>
+ *   <li>With {@link #resolveAsVariable} enabled (recommended): {@code 
#{hop-pwd:SFTP_PASSWORD}}
+ *       looks up variable {@code SFTP_PASSWORD}, then decrypts its value.
+ *   <li>With {@link #resolveAsVariable} disabled: {@code #{hop-pwd:Encrypted 
abcd...}} decrypts the
+ *       literal argument.
+ * </ul>
+ *
+ * <p>This is an interoperability helper for trusted operators. The default 
Hop password encoder is
+ * obfuscation, not strong encryption; use AES2 or a secrets manager for real 
confidentiality.
+ */
+@Getter
+@Setter
+@GuiPlugin
+@VariableResolverPlugin(
+    id = "HopPassword",
+    name = "Hop Password Variable Resolver",
+    description =
+        "Decrypt Hop-encoded passwords (Encrypted / AES2) for use in 
expressions such as VFS URIs",
+    documentationUrl = 
"/metadata-types/variable-resolver/hop-password-variable-resolver.html")
+public class HopPasswordVariableResolver implements IVariableResolver {
+
+  /**
+   * When true, the expression argument after the first colon is treated as a 
variable name whose
+   * value is looked up and then decrypted. When false, the argument is 
decrypted as a literal
+   * encoded password string.
+   */
+  @GuiWidgetElement(
+      id = "resolveAsVariable",
+      order = "01",
+      label = "i18n::HopPasswordVariableResolver.label.resolveAsVariable",
+      toolTip = "i18n::HopPasswordVariableResolver.tooltip.resolveAsVariable",
+      type = GuiElementType.CHECKBOX,
+      parentId = VariableResolver.GUI_PLUGIN_ELEMENT_PARENT_ID)
+  @HopMetadataProperty
+  private boolean resolveAsVariable = true;
+
+  /**
+   * When true and {@link #resolveAsVariable} is enabled, throw if the named 
variable is not
+   * defined. When false (default), a missing variable yields a null/empty 
resolution (expression
+   * left unchanged by the variable system).
+   */
+  @GuiWidgetElement(
+      id = "failIfVariableNotDefined",
+      order = "02",
+      label = 
"i18n::HopPasswordVariableResolver.label.failIfVariableNotDefined",
+      toolTip = 
"i18n::HopPasswordVariableResolver.tooltip.failIfVariableNotDefined",
+      type = GuiElementType.CHECKBOX,
+      parentId = VariableResolver.GUI_PLUGIN_ELEMENT_PARENT_ID)
+  @HopMetadataProperty
+  private boolean failIfVariableNotDefined = false;
+
+  @Override
+  public void init() {
+    // Nothing to initialize.
+  }
+
+  @Override
+  public String getPluginId() {
+    return "HopPassword";
+  }
+
+  @Override
+  public String getPluginName() {
+    return "Hop Password Variable Resolver";
+  }
+
+  @Override
+  public String resolve(String secretPath, IVariables variables) throws 
HopException {
+    if (StringUtils.isEmpty(secretPath)) {
+      return secretPath;
+    }
+
+    String encodedValue;
+    if (resolveAsVariable) {
+      String variableName = secretPath.trim();
+      // Allow optional ${VAR} form if someone passes it literally.
+      if (variableName.startsWith("${")
+          && variableName.endsWith("}")
+          && variableName.length() > 3) {
+        variableName = variableName.substring(2, variableName.length() - 1);
+      }
+      encodedValue = variables.getVariable(variableName);
+      if (encodedValue == null) {
+        if (failIfVariableNotDefined) {
+          throw new HopException("Password variable '" + variableName + "' is 
not defined");
+        }
+        return null;
+      }
+    } else {
+      encodedValue = secretPath;
+    }
+
+    if (StringUtils.isEmpty(encodedValue)) {
+      return encodedValue;
+    }
+
+    return Encr.decryptPasswordOptionallyEncrypted(encodedValue);
+  }
+}
diff --git 
a/plugins/misc/passwords/src/main/resources/org/apache/hop/passwords/resolver/messages/messages_en_US.properties
 
b/plugins/misc/passwords/src/main/resources/org/apache/hop/passwords/resolver/messages/messages_en_US.properties
new file mode 100644
index 0000000000..32106edc49
--- /dev/null
+++ 
b/plugins/misc/passwords/src/main/resources/org/apache/hop/passwords/resolver/messages/messages_en_US.properties
@@ -0,0 +1,21 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+#       http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+#
+HopPasswordVariableResolver.label.resolveAsVariable = Resolve argument as a 
variable name
+HopPasswordVariableResolver.tooltip.resolveAsVariable = When enabled, the 
value after the colon is treated as a variable name. That variable is looked up 
and its value is decrypted. When disabled, the argument is decrypted as a 
literal (for example Encrypted \u2026 or AES2 \u2026). Prefer the variable-name 
form for cleaner expressions such as '#{hop-pwd:SFTP_PASSWORD}'.
+HopPasswordVariableResolver.label.failIfVariableNotDefined = Fail if variable 
is not defined
+HopPasswordVariableResolver.tooltip.failIfVariableNotDefined = When enabled 
together with "Resolve argument as a variable name", an undefined variable name 
causes an error (pipeline/workflow fails). When disabled, a missing variable is 
ignored and the original expression is left unchanged.
diff --git 
a/plugins/misc/passwords/src/test/java/org/apache/hop/passwords/resolver/HopPasswordVariableResolverTest.java
 
b/plugins/misc/passwords/src/test/java/org/apache/hop/passwords/resolver/HopPasswordVariableResolverTest.java
new file mode 100644
index 0000000000..0bf68838bb
--- /dev/null
+++ 
b/plugins/misc/passwords/src/test/java/org/apache/hop/passwords/resolver/HopPasswordVariableResolverTest.java
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ *
+ */
+
+package org.apache.hop.passwords.resolver;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import org.apache.hop.core.Const;
+import org.apache.hop.core.HopClientEnvironment;
+import org.apache.hop.core.encryption.Encr;
+import org.apache.hop.core.encryption.HopTwoWayPasswordEncoder;
+import org.apache.hop.core.exception.HopException;
+import org.apache.hop.core.variables.Variables;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class HopPasswordVariableResolverTest {
+
+  private HopPasswordVariableResolver resolver;
+  private Variables variables;
+
+  @BeforeAll
+  static void initEnvironment() throws Exception {
+    HopClientEnvironment.init();
+  }
+
+  @BeforeEach
+  void setUp() throws Exception {
+    // Re-bind the default Hop encoder so this class is not polluted by AES 
encoder tests that
+    // share the JVM (surefire reuseForks).
+    System.setProperty(Const.HOP_PASSWORD_ENCODER_PLUGIN, "Hop");
+    Encr.init("Hop");
+    resolver = new HopPasswordVariableResolver();
+    variables = new Variables();
+    variables.initializeFrom(null);
+  }
+
+  @Test
+  void pluginMetadata() {
+    assertEquals("HopPassword", resolver.getPluginId());
+    assertEquals("Hop Password Variable Resolver", resolver.getPluginName());
+    assertTrue(resolver.isResolveAsVariable());
+    assertFalse(resolver.isFailIfVariableNotDefined());
+  }
+
+  @Test
+  void resolveLiteralEncryptedPassword() throws Exception {
+    resolver.setResolveAsVariable(false);
+    String plain = "mypass";
+    String encoded = Encr.encryptPasswordIfNotUsingVariables(plain);
+
+    assertEquals(plain, resolver.resolve(encoded, variables));
+  }
+
+  @Test
+  void resolveLiteralPlaintextPassthrough() throws Exception {
+    resolver.setResolveAsVariable(false);
+    assertEquals("already-plain", resolver.resolve("already-plain", 
variables));
+  }
+
+  @Test
+  void resolveVariableNameToEncryptedValue() throws Exception {
+    resolver.setResolveAsVariable(true);
+    String plain = "sftp-secret";
+    String encoded = Encr.encryptPasswordIfNotUsingVariables(plain);
+    variables.setVariable("SFTP_PASSWORD", encoded);
+
+    assertEquals(plain, resolver.resolve("SFTP_PASSWORD", variables));
+  }
+
+  @Test
+  void resolveVariableNameWithWhitespace() throws Exception {
+    resolver.setResolveAsVariable(true);
+    String plain = "trim-me";
+    String encoded = Encr.encryptPasswordIfNotUsingVariables(plain);
+    variables.setVariable("PWD", encoded);
+
+    assertEquals(plain, resolver.resolve("  PWD  ", variables));
+  }
+
+  @Test
+  void resolveMissingVariableReturnsNullByDefault() throws Exception {
+    resolver.setResolveAsVariable(true);
+    resolver.setFailIfVariableNotDefined(false);
+    assertNull(resolver.resolve("DOES_NOT_EXIST", variables));
+  }
+
+  @Test
+  void resolveMissingVariableThrowsWhenFailEnabled() {
+    resolver.setResolveAsVariable(true);
+    resolver.setFailIfVariableNotDefined(true);
+    HopException ex =
+        assertThrows(HopException.class, () -> 
resolver.resolve("DOES_NOT_EXIST", variables));
+    assertTrue(ex.getMessage().contains("DOES_NOT_EXIST"));
+  }
+
+  @Test
+  void resolveOptionalDollarBraceVariableForm() throws Exception {
+    resolver.setResolveAsVariable(true);
+    String plain = "brace-form";
+    String encoded = Encr.encryptPasswordIfNotUsingVariables(plain);
+    variables.setVariable("SFTP_PASSWORD", encoded);
+
+    assertEquals(plain, resolver.resolve("${SFTP_PASSWORD}", variables));
+  }
+
+  @Test
+  void resolveEmptyArgument() throws Exception {
+    assertNull(resolver.resolve(null, variables));
+    assertEquals("", resolver.resolve("", variables));
+  }
+
+  @Test
+  void resolveUsesHopEncryptedPrefix() throws Exception {
+    resolver.setResolveAsVariable(false);
+    HopTwoWayPasswordEncoder encoder = new HopTwoWayPasswordEncoder();
+    String encoded = encoder.encode("abcd", true);
+    
assertTrue(encoded.startsWith(HopTwoWayPasswordEncoder.PASSWORD_ENCRYPTED_PREFIX));
+    assertEquals("abcd", resolver.resolve(encoded, variables));
+  }
+}

Reply via email to