Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@


--------------------------------------------------------------------------------
Fix
--------------------------------------------------------------------------------
* yang.ncdiff
* NETCONF masking to hide encrypted values in get-config while preserving unmasked data for edit-config.
4 changes: 3 additions & 1 deletion ncdiff/src/yang/ncdiff/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -566,9 +566,11 @@ def device(self):

@property
def nc(self):
src = getattr(self.config_src, "ele_original", self.config_src.ele)
dst = getattr(self.config_dst, "ele_original", self.config_dst.ele)
return NetconfCalculator(
self.device,
self.config_dst.ele, self.config_src.ele,
dst, src,
preferred_create=self.preferred_create,
preferred_replace=self.preferred_replace,
preferred_delete=self.preferred_delete,
Expand Down
49 changes: 47 additions & 2 deletions ncdiff/src/yang/ncdiff/device.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import os
import re

from copy import deepcopy
import logging
from lxml import etree
from ncclient import manager, operations, transport, xml_
Expand Down Expand Up @@ -361,7 +361,7 @@ def load_model(self, model):
else:
logger.info(f'Model {model} is already loaded')
return self.models[model]

def execute(self, operation, *args, **kwargs):
'''execute

Expand Down Expand Up @@ -503,6 +503,39 @@ def take_notification(self, block=True, timeout=None):
isinstance(reply, transport.notify.Notification):
reply.ns = self._get_ns(reply._root_ele)
return reply

def _mask_encrypted_values(self, element):
"""
This function walks the given XML element tree and replaces sensitive
encrypted values with the placeholder string "ENCRYPTED". Masking is applied
only to GET/GET-CONFIG output.

Note:
Masking is used only for output visibility. Internal diff/edit-config
logic must operate on unmasked values to avoid device-side failures.

Returns:

None. The input element tree is modified in place.
"""

for node in element.iter():
if not node.text:
continue

text = node.text.strip()
tag = node.tag.lower()

# Mask all SNMPv3 auth/priv passwords and secrets explicitly
if "snmp" in tag and ("password" in tag or "secret" in tag):
node.text = "ENCRYPTED"
continue

# IOS-XE encrypted secrets are ALWAYS long (Type 6)
if ("password" in tag or "secret" in tag) and len(text) > 12:
node.text = "ENCRYPTED"
continue


def extract_config(self, reply, type='netconf'):
'''extract_config
Expand Down Expand Up @@ -551,6 +584,18 @@ def remove_read_only(parent):

config = Config(self, reply)
remove_read_only(config.ele)

# deepcopy of config.ele to store unmasked ele_original
config.ele_original = deepcopy(config.ele)

if type == 'netconf':
# Apply masking only to NETCONF get/get-config output
masked_copy = deepcopy(config.ele_original)
self._mask_encrypted_values(masked_copy)
config.ele = masked_copy
else:
# Use unmasked config for all other operations
config.ele = config.ele_original
return config

def get_schema_node(self, config_node):
Expand Down