App-Netdisco

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN


  [BUG FIXES]

  * #1649 fix the API node search returning null instead of node data

2.105002 - 2026-08-24

  [ENHANCEMENTS]

  * #1643 sort the Ports query in SQL instead of Perl, matching portsort.js
  * #1644 replace he.js with the DataTables escaper already on every page
  * #1645 compress web responses, with a compress_responses setting to disable it
  * #1646 load the admin task panes with htmx
  * update Python dependencies and GitHub Actions versions

  [BUG FIXES]

  * #1645 restore the cache header on JavaScript
  * #1646 refresh the users pane when the API token reveal modal is dismissed

2.105001 - 2026-08-22

  [BUG FIXES]

  * escape the results payload for its script context
  * keep the sidebar above a results table that overflows its column

2.105000 - 2026-08-20

  [NEW FEATURES]

  * devices file param to netdisco-do now requires @ prefix
  * #1636 add device port support to field_protection for failed SNMP walks

  [ENHANCEMENTS]

Changes  view on Meta::CPAN

  * #1584 make GET /api/v1/search/device batch ready with field selection
  * #1591 new option expire_nodeip_orphans to keep node_ip entries without node

  [ENHANCEMENTS]

  * update Python dependencies and GitHub Actions versions
  * use canonical URLs for upstream-sources raw files

  [BUG FIXES]

  * #1583 port lookup in API: uri_unescape and trim captured port name
  * #1585 uninitialized value warning when daterange param is absent
  * #1586 /api/ endpoints returning HTML when Accept header is missing
  * #1607 Refuse to empty snmp_object when the MIB reports parse to nothing
  * #1609 netdisco-deploy: report loadmibs failure instead of done

2.101000 - 2026-07-02

  [NEW FEATURES]

  * #1569 new option snmp_fast_connect_timeout instead of static 200ms value

Changes  view on Meta::CPAN

  * Remove Daemon's job queue DBIC schema from user config
  * Add log messages to the Daemon
  * Use Path::Class for path and file name construction consistently
  * All links and redirects are now relative
  * More documentation on developing and deployment

  [BUG FIXES]

  * Avoid use of DNS when looking up devices in DB by IP
  * Search by device port MAC no longer fatal
  * URI and HTML escape template variables
  * Prevent duplicate requets caused by favicon.ico link
  * DB Schema upgrades are now all single-step in case user has applied any already

2.005000_002 - 2013-02-10

  [ENHANCEMENTS]

  * When linking to devices, use the DNS name in preference to the IP
    (makes for more attractive navigation search bar, but need to watch for aliases)
  * In general, device names do not have domain_suffix trimmed, but nodes do

MANIFEST  view on Meta::CPAN

xt/21-check_acl_no_ipaddr_only.t
xt/30-backend-workers.t
xt/31-loadmibsfromoids-guard.t
xt/32-swagger-ui-guard.t
xt/33-vendored-manifest.t
xt/34-module-tree-order.t
xt/35-login-return-url.t
xt/36-ajax-content-response.t
xt/37-typeahead-empty-query.t
xt/38-swagger-ui-redirect.t
xt/39-html-escaper.t
xt/40-template-snapshots.t
xt/41-script-json-escape.t
xt/42-subinterface-grouping-scan.t
xt/43-response-compression.t
xt/44-results-token-escaping.t
xt/45-netmap-neighbor-walk.t
xt/46-native-history-api.t
xt/47-site-local-checker.t
xt/48-htmx-form-attributes.t
xt/49-netmap-position-key.t
xt/50-config-override.t
xt/51-updated-device-hook.t

lib/App/Netdisco/DB/Result/Device.pm  view on Meta::CPAN


  $schema->resultset('DevicePort')
    ->search({remote_ip => $old_ip})
    ->update({remote_ip => $new_ip});

  $schema->resultset('Node')
    ->search({switch => $old_ip})
    ->update({switch => $new_ip});

  # this whole shenanigans exists because I cannot work out how to
  # pass an escaped SQL placeholder into DBIx::Class/SQL::Abstract
  # see https://www.mail-archive.com/dbix-class@lists.scsys.co.uk/msg07079.html
  $schema->storage->dbh_do(sub {
    my ($storage, $dbh, @extra) = @_;
    local $dbh->{TraceLevel} = ($ENV{DBIC_TRACE} ? '1|SQL' : $dbh->{TraceLevel});

    my $router_first_sql = q{
      UPDATE node_ip
        SET seen_on_router_first = seen_on_router_first - ?::text || jsonb_build_object(?::text, seen_on_router_first -> ?::text)
        WHERE seen_on_router_first \? ?::text
    };

lib/App/Netdisco/Util/SiteLocal.pm  view on Meta::CPAN

# One row per shipped removal. A later rung adds a row here and nothing else.
#
# `pattern` is matched against each line of each file under the scanned paths.
# `release` is the release that removed or deprecated the thing, for the report
# to cite.
my @RULES = (
  {
    name    => 'he-js',
    release => '2.105002',
    pattern => qr/\bhe\s*\.\s*(?:encode|decode)\s*\(/,
    advice  => 'he.js was removed. Use DataTable.util.escapeHtml(), which is '
             . 'loaded on every page already.',
  },
  {
    name    => 'history-js',
    release => '2.105004',
    # History with a capital H is history.js. The browser's own object is
    # lowercase, so the case distinction is the whole test. window.History
    # still exists after the removal, as the DOM interface constructor, so a
    # site's `window.History && ...` guard stops running its branch and never
    # throws. That silence is why this rule matters more than it looks.

lib/App/Netdisco/Util/Web.pm  view on Meta::CPAN

  interval_to_daterange
  sql_match
  request_is_device
  request_is_api
  request_is_api_report
  request_is_api_search
  device_display_name
  page_title
  pane_chrome
  pane_history_header
  escape_for_script_context
  escape_results_token
/;
our %EXPORT_TAGS = (all => \@EXPORT_OK);

=head1 NAME

App::Netdisco::Util::Web

=head1 DESCRIPTION

A set of helper subroutines to support parts of the Netdisco application.

lib/App/Netdisco/Util/Web.pm  view on Meta::CPAN

    return '' unless $config->{'provides_csv'};
    return '' if $tab eq 'portlog' or $tab eq 'jobqueue';
  }

  my $query = request->env->{'QUERY_STRING'} || '';
  my $href = uri_for("/ajax/content/$page/$tab")->path
    . (length $query ? ('?'. $query) : '');

  return _oob_anchor('nd_csv-download', $href,
    ($config->{'provides_csv'} ? '' : ' hidden'),
    ' download="'. _escape_attr("netdisco-$page-$tab.csv") .'"',
    '<i id="nd_csv-download-icon" class="text-info far fa-file-lines fa-lg"'
    .' rel="tooltip" data-bs-placement="left" data-bs-title="Download as CSV"></i>');
}

# the fields each device tab offers, in the order the sidebar renders them, so
# that the reset address reads the way it did when the browser built it
my %RESET_FIELDS = (
  ports  => [qw/q f partial invert/],
  netmap => [qw/q/],
);

lib/App/Netdisco/Util/Web.pm  view on Meta::CPAN

  return _oob_anchor('nd_sidebar-reset-link', $uri->as_string, '', '',
    '<i class="nd_sidebar-reset fas fa-arrow-rotate-left"'
    .' rel="tooltip" data-bs-placement="left" data-bs-title="Reset to Defaults"'
    .' data-bs-container="body"></i>');
}

sub _oob_anchor {
  my ($id, $href, $hidden, $extra, $content) = @_;

  return '<a id="'. $id .'" hx-swap-oob="true"'. $hidden
    .' href="'. _escape_attr($href) .'"'. $extra .'>'. $content .'</a>';
}

sub _escape_attr {
  return HTML::Entities::encode_entities(shift, q{<>&"'});
}

=head2 escape_for_script_context( $json )

Makes a JSON string safe to embed as a JavaScript literal inside an HTML
C<< <script> >> element, which is how the report and search templates ship
their result sets. Returns anything that is not a defined plain scalar
unchanged, so a resultset or an arrayref passed to a CSV or API template is
left alone.

=cut

sub escape_for_script_context {
  my $json = shift;
  return $json if (not defined $json) or ref $json;

  # Inside a script element the HTML parser ends the element at "</" and
  # changes state at "<!--" and "<script", none of which it stops doing just
  # because the sequence sits inside a JavaScript string. Escaping every "<"
  # covers all three. Both JSON.parse and the JavaScript parser read the
  # escape back as "<", so the data the page receives is unchanged.
  $json =~ s/</\\u003C/g;

  # Not an HTML concern. This JSON is emitted as JavaScript source rather than
  # parsed from a string, and these two characters are line terminators there.
  $json =~ s/\x{2028}/\\u2028/g;
  $json =~ s/\x{2029}/\\u2029/g;

  return $json;
}

=head2 escape_results_token( $tokens )

Applies C<escape_for_script_context> to the C<results> token of a template
token hash, in place, and returns the hash.

Only when the key is already there. The API serializer chooses between
emitting C<results> alone and walking the whole token hash on C<exists
$tokens-E<gt>{results}>, and a handler that renders without that token, as
node search does, needs the second. Assigning unconditionally would
autovivify the key and silently move such a handler onto the first branch.

=cut

sub escape_results_token {
  my $tokens = shift;
  return $tokens unless (ref {} eq ref $tokens) and exists $tokens->{results};

  $tokens->{results} = escape_for_script_context( $tokens->{results} );
  return $tokens;
}

1;

lib/App/Netdisco/Web.pm  view on Meta::CPAN

use Dancer::Plugin::Auth::Extensible;
use Dancer::Plugin::Swagger;

use Dancer::Error;
use Dancer::Continuation::Route::ErrorSent;

use URI ();
use Socket6 (); # to ensure dependency is met
use HTML::Entities (); # to ensure dependency is met
use URI::QueryParam (); # part of URI, to add helper methods
use URI::Escape 'uri_escape_utf8';
use MIME::Base64 'encode_base64';
use Path::Class 'dir';
use Module::Load ();
use Data::Visitor::Tiny;
use Scalar::Util 'blessed';
use Storable 'dclone';
use URI::Based;

use App::Netdisco::Util::Web qw/
  escape_results_token
  interval_to_daterange
  page_title
  pane_chrome
  pane_history_header
  request_is_api
  request_is_api_report
  request_is_api_search
/;
use App::Netdisco::Util::Permission qw/acl_matches acl_matches_only/;
use App::Netdisco::Util::SiteLocal qw/scan_shadowed_files site_local_paths/;

lib/App/Netdisco/Web.pm  view on Meta::CPAN

      my $forwarded = $self->forwarded_for_address;
      my ($client) = (defined $forwarded ? ($forwarded =~ m/([^,\s]+)\s*$/) : ());
      return ($client || $self->env->{REMOTE_ADDR});
  };

  # to insert /t/$tenant if set
  # which is fine for building links, but not fine for
  # comparison to request->path, because when is_forward() the
  # request->path is changed...
  *Dancer::Request::uri_for = sub {
    my ($self, $part, $params, $dont_escape) = @_;
    my $uri = $self->base;

    if (vars->{'tenant'}) {
        $part = '/t/'. vars->{'tenant'} . $part;
    }

    # Make sure there's exactly one slash between the base and the new part
    my $base = $uri->path;
    $base =~ s|/$||;
    $part =~ s|^/||;
    $uri->path("$base/$part");

    $uri->query_form($params) if $params;

    return $dont_escape ? uri_unescape($uri->canonical) : $uri->canonical;
  };

  # ...so here we are monkeypatching request->path as well
  *Dancer::Request::path = sub {
    die "path is accessor not mutator" if scalar @_ > 1;
    my $self = shift;
    $self->_build_path() unless $self->{path};

    if (vars->{'tenant'} and $self->{path} !~ m{/t/}) {
        my $path = $self->{path};

lib/App/Netdisco/Web.pm  view on Meta::CPAN


hook 'before_template' => sub {
    my $tokens = shift;

    # quick b64 encode
    $tokens->{atob} = sub { encode_base64(shift, '') };

    # allow portable static content
    $tokens->{uri_base} = request->base->path
      if request->base->path ne '/';
    $tokens->{uri_base} .= ('/t/'. uri_escape_utf8(vars->{'tenant'}))
      if vars->{'tenant'};

    # cache-busting suffix for the stylesheets and scripts in the layout.
    # bin/netdisco-web-fg asks browsers to hold css, javascript, images and
    # fonts for a day without revalidating, and the filenames never carry a
    # version, so after an upgrade a returning visitor renders the new markup
    # with the previous release's stylesheet until that day is up.
    $tokens->{asset_version} = ($App::Netdisco::VERSION || 'HEAD');

    # allow portable dynamic content

lib/App/Netdisco/Web.pm  view on Meta::CPAN

    # allow hash keys with leading underscores
    $Template::Stash::PRIVATE = undef;
};

# The report and search templates embed their result set as a JavaScript
# literal inside a <script> element, which they must opt out of AutoFilter to
# do. Escaping happens here rather than in each of the templates so that a
# site-local template or a third-party report plugin gets it too, without its
# author having to know.
hook 'before_template' => sub {
    escape_results_token( shift );
};

# prevent Template::AutoFilter taking action on CSV output
hook 'before_template' => sub {
    my $template_engine = engine 'template';
    if (not request->is_ajax
        and header('Content-Type')
        and header('Content-Type') eq 'text/comma-separated-values' ) {

        $template_engine->{config}->{AUTO_FILTER} = 'none';

lib/App/Netdisco/Web/API/Objects.pm  view on Meta::CPAN

package App::Netdisco::Web::API::Objects;

use Dancer ':syntax';
use Dancer::Plugin::DBIC;
use Dancer::Plugin::Swagger;
use Dancer::Plugin::Auth::Extensible;

use App::Netdisco::JobQueue 'jq_insert';
use Try::Tiny;
use URI::Escape 'uri_unescape';

sub _port_param { my $s = uri_unescape(shift // ''); $s =~ s/^\s+|\s+$//g; $s }

swagger_path {
  tags => ['Objects'],
  path => (setting('api_base') || '').'/object/device/{ip}',
  description => 'Returns a row from the device table',
  parameters  => [
    ip => {
      description => 'Canonical IP of the Device. Use Search methods to find this.',
      required => 1,
      in => 'path',

lib/App/Netdisco/Web/API/Objects.pm  view on Meta::CPAN

        port => {
          description => 'Name of the port. Use the ".../device/{ip}/ports" method to find these.',
          required => 1,
          in => 'path',
        },
      ],
      responses => { default => {} },
    }, get qr{/api/v1/object/device/(?<ip>[^/]+)/port/(?<port>.+)/${rel}$} => require_role api => sub {
      my $params = captures;
      my $rows = try { schema(vars->{'tenant'})->resultset('DevicePort')
        ->find( _port_param($$params{port}), uri_unescape($$params{ip}) )->$rel }
        or send_error('Bad Device or Port', 404);
      return to_json [ map {$_->TO_JSON} $rows->all ];
    };
}

foreach my $rel (qw/power properties wireless agg_master neighbor last_node/) {
    swagger_path {
      tags => ['Objects'],
      description => "Returns the related $rel table entry for a given port",
      path => (setting('api_base') || '')."/object/device/{ip}/port/{port}/$rel",

lib/App/Netdisco/Web/API/Objects.pm  view on Meta::CPAN

        port => {
          description => 'Name of the port. Use the ".../device/{ip}/ports" method to find these.',
          required => 1,
          in => 'path',
        },
      ],
      responses => { default => {} },
    }, get qr{/api/v1/object/device/(?<ip>[^/]+)/port/(?<port>.+)/${rel}$} => require_role api => sub {
      my $params = captures;
      my $row = try { schema(vars->{'tenant'})->resultset('DevicePort')
        ->find( _port_param($$params{port}), uri_unescape($$params{ip}) )->$rel }
        or send_error('Bad Device or Port', 404);
      return to_json $row->TO_JSON;
    };
}

# must come after the port methods above, so the route matches later
swagger_path {
  tags => ['Objects'],
  description => 'Returns a row from the device_port table',
  path => (setting('api_base') || '').'/object/device/{ip}/port/{port}',

lib/App/Netdisco/Web/API/Objects.pm  view on Meta::CPAN

    port => {
      description => 'Name of the port. Use the ".../device/{ip}/ports" method to find these.',
      required => 1,
      in => 'path',
    },
  ],
  responses => { default => {} },
}, get qr{/api/v1/object/device/(?<ip>[^/]+)/port/(?<port>.+)$} => require_role api => sub {
  my $params = captures;
  my $port = try { schema(vars->{'tenant'})->resultset('DevicePort')
    ->find( _port_param($$params{port}), uri_unescape($$params{ip}) ) }
    or send_error('Bad Device or Port', 404);
  return to_json $port->TO_JSON;
};

swagger_path {
  tags => ['Objects'],
  path => (setting('api_base') || '').'/object/device/{ip}/nodes',
  description => "Returns the nodes found on a given Device",
  parameters  => [
    ip => {

lib/App/Netdisco/Web/Plugin/Device/Ports.pm  view on Meta::CPAN

use Dancer::Plugin::DBIC;
use Dancer::Plugin::Auth::Extensible;

use App::Netdisco::Util::Permission 'acl_matches';
use App::Netdisco::Util::Port qw/port_acl_service port_acl_pvid port_acl_name/;
use App::Netdisco::Web::Plugin;

use List::MoreUtils 'singleton';
use NetAddr::MAC ();
use DBIx::Class::ResultClass::HashRefInflator;
use URI::Escape 'uri_escape';

register_device_tab({ tag => 'ports', label => 'Ports', provides_csv => 1 });

# $nodes_name names a has_many relation, but the source it reads from is a
# different name again.
my %node_result_class = (
    nodes                 => 'Node',
    active_nodes          => 'Virtual::ActiveNode',
    nodes_with_age        => 'Virtual::NodeWithAge',
    active_nodes_with_age => 'Virtual::ActiveNodeWithAge',

lib/App/Netdisco/Web/Plugin/Device/Ports.pm  view on Meta::CPAN


    my $qs = '';
    $qs .= '&n_ip4=1' if $n_ip4;
    $qs .= '&n_ip6=1' if $n_ip6;
    $qs .= '&n_dns=1' if $n_dns;
    $qs .= '&n_age=1' if $n_age;
    $qs .= '&n_ssid=1' if $n_ssid;
    $qs .= '&n_vendor=1' if $n_vendor;
    $qs .= '&n_netbios=1' if $n_netbios;
    $qs .= '&n_archived=1' if $n_archived;
    $qs .= '&mac_format=' . uri_escape($mac_format) if $mac_format;
    return $qs;
}

# Ports at or under the threshold get their own row fetched; a port over it
# still gets one when it might carry a neighbor's own MAC, since the
# Neighbors column reads stitched_nodes for that regardless of the node
# list's own threshold, but only under c_neighbors: that carve-out is what
# makes neighbor_mac renderable at all, and stitching every node of an
# over-threshold trunk port just to throw the render away is expensive on
# exactly the ports a core switch has the most of. Kept free of param() and

lib/App/Netdisco/Web/Plugin/Report/PortVLANMismatch.pm  view on Meta::CPAN


get '/ajax/content/report/portvlanmismatch' => require_login sub {
    return unless schema(vars->{'tenant'})->resultset('Device')->count;
    my @results = schema(vars->{'tenant'})
      ->resultset('Virtual::PortVLANMismatch')->search({},{
          bind => [ setting('sidebar_defaults')->{'device_ports'}->{'p_hide1002'}->{'default'}
                      ? (1002, 1003, 1004, 1005) : (0, 0, 0, 0) ],
      })
      ->hri->all;

#    # note that the generated list is rendered without HTML escape,
#    # so we MUST sanitise here with the grep
#    foreach my $res (@results) {
#        my @left  = grep {m/^(?:n:)?\d+$/} map {s/\s//g; $_} split ',', $res->{left_vlans};
#        my @right = grep {m/^(?:n:)?\d+$/} map {s/\s//g; $_} split ',', $res->{right_vlans};
#
#        my %new = (0 => [], 1 => []);
#        my %cmp = listcmp @left, @right;
#        foreach my $vlan (keys %cmp) {
#            map { push @{ $new{$_} }, ( (2 == scalar @{ $cmp{$vlan} }) ? $vlan : "<strong>$vlan</strong>" ) } @{ $cmp{$vlan} };
#        }

share/contrib/raddb/dictionary.usr  view on Meta::CPAN

VALUE	Initial-Modulation-Type		clearChannel			28
VALUE	Initial-Modulation-Type		x2client			29
VALUE	Initial-Modulation-Type		x2symmetric			30
VALUE	Initial-Modulation-Type		piafs				31
VALUE	Initial-Modulation-Type		x2version2			32
VALUE	Initial-Modulation-Type		v90Analogue			33
VALUE	Initial-Modulation-Type		v90Digital			34
VALUE	Initial-Modulation-Type		v90AllDigital			35

VALUE	USR-Connect-Term-Reason	dtrDrop				1
VALUE	USR-Connect-Term-Reason	escapeSequence			2
VALUE	USR-Connect-Term-Reason	athCommand			3
VALUE	USR-Connect-Term-Reason	carrierLoss			4
VALUE	USR-Connect-Term-Reason	inactivityTimout		5
VALUE	USR-Connect-Term-Reason	mnpIncompatible			6
VALUE	USR-Connect-Term-Reason	undefined			7
VALUE	USR-Connect-Term-Reason	remotePassword			8
VALUE	USR-Connect-Term-Reason	linkPassword			9
VALUE	USR-Connect-Term-Reason	retransmitLimit			10
VALUE	USR-Connect-Term-Reason	linkDisconnectMsgReceived	11
VALUE	USR-Connect-Term-Reason	noLoopCurrent			12

share/contrib/raddb/dictionary.usr  view on Meta::CPAN

VALUE   USR-Connect-Term-Reason	outgoingEMWinkTimeout           91
VALUE   USR-Connect-Term-Reason	outgoingEMWinkTooShort          92
VALUE   USR-Connect-Term-Reason	outgoingNoChannelAvail          93
VALUE   USR-Connect-Term-Reason	dspReboot               94
VALUE   USR-Connect-Term-Reason	noDSPRespToKA           95
VALUE   USR-Connect-Term-Reason	noDSPRespToDisc         96
VALUE   USR-Connect-Term-Reason	dspTailPtrInvalid               97
VALUE   USR-Connect-Term-Reason	dspHeadPtrInvalid               98

VALUE	USR-Failure-to-Connect-Reason	dtrDrop			1
VALUE	USR-Failure-to-Connect-Reason	escapeSequence		2
VALUE	USR-Failure-to-Connect-Reason	athCommand		3
VALUE	USR-Failure-to-Connect-Reason	carrierLoss		4
VALUE	USR-Failure-to-Connect-Reason	inactivityTimout	5
VALUE	USR-Failure-to-Connect-Reason	mnpIncompatible		6
VALUE	USR-Failure-to-Connect-Reason	undefined		7
VALUE	USR-Failure-to-Connect-Reason	remotePassword		8
VALUE	USR-Failure-to-Connect-Reason	linkPassword		9
VALUE	USR-Failure-to-Connect-Reason	retransmitLimit		10
VALUE	USR-Failure-to-Connect-Reason	linkDisconnectMsgRec	11
VALUE	USR-Failure-to-Connect-Reason	noLoopCurrent		12

share/contrib/raddb/dictionary.usr  view on Meta::CPAN

VALUE	USR-RMMIE-x2-Status			incompatibleModes	10
VALUE	USR-RMMIE-x2-Status			local3200Disabled	11
VALUE	USR-RMMIE-x2-Status			excessHighFrequencyAtten	12
VALUE	USR-RMMIE-x2-Status			connectNotSupport3200	13
VALUE	USR-RMMIE-x2-Status			retrainBeforeConnection	14

VALUE	USR-RMMIE-Planned-Disconnect		none			1
VALUE	USR-RMMIE-Planned-Disconnect		dteNotReady		2
VALUE	USR-RMMIE-Planned-Disconnect		dteInterfaceError	3
VALUE	USR-RMMIE-Planned-Disconnect		dteRequest		4
VALUE	USR-RMMIE-Planned-Disconnect		escapeToOnlineCommandMode	5
VALUE	USR-RMMIE-Planned-Disconnect		athCommand		6
VALUE	USR-RMMIE-Planned-Disconnect		inactivityTimeout	7
VALUE	USR-RMMIE-Planned-Disconnect		arqProtocolError	8
VALUE	USR-RMMIE-Planned-Disconnect		arqProtocolRetransmitLim	9
VALUE	USR-RMMIE-Planned-Disconnect		invalidComprDataCodeword	10
VALUE	USR-RMMIE-Planned-Disconnect		invalidComprDataStringLen	11
VALUE	USR-RMMIE-Planned-Disconnect		invalidComprDataCommand	12

VALUE	USR-RMMIE-Last-Update-Event		none			1
VALUE	USR-RMMIE-Last-Update-Event		initialConnection	2

share/public/javascripts/bootstrap.min.js  view on Meta::CPAN

  */
/*!
 * @popperjs/core v2.11.8 - MIT License
 *
 * Copyright (c) 2019 Federico Zivolo
 *
 * Popper is inlined into this file by Bootstrap's bundle build, which emits
 * only the banner above it. This notice is reproduced from the @popperjs/core
 * package so that the licence travels with the code it covers.
 */
!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?module.exports=e():"function"==typeof define&&define.amd?define(e):(t="undefined"!=typeof globalThis?globalThis:t||self).bootstrap=e()}(this,function(){"use strict";const t=new Map,e...
//# sourceMappingURL=bootstrap.bundle.min.js.map

share/public/javascripts/dataTables.min.js  view on Meta::CPAN

/*! DataTables 3.0.3
 * Copyright (c) SpryMedia Ltd - datatables.net/license
 */
(t=>{"function"==typeof define&&define.amd?define([],function(){return t(window,document)}):"object"==typeof exports?"undefined"==typeof window?module.exports=function(e){return e=e||window,t(e,e.document)}:module.exports=t(window,window.document):wi...

share/public/javascripts/htmx.min.js  view on Meta::CPAN

var htmx=(()=>{const e={parse(t){if(!t)return{};if(t.startsWith("{"))return JSON.parse(t);let r=/(?:"([^"]+)"|'([^']+)'|([^\s,:]+))(?:\s*:\s*(?:"([^"]*)"|'([^']*)'|<((?:[^/]|\/(?!>))+)\/>|([^\s,]+)))?(?=\s|,|$)/g,i={};for(let s of t.matchAll(r)){let[...

share/public/javascripts/netdisco-tables.js  view on Meta::CPAN

// the id of the application/json block holding the rows, and data-nd-admin
// tells the subnet renderer whether to show the admin tools.
//
// JSON cannot carry a function, so a column's render and a table's
// drawCallback or initComplete are named here and referenced by name. Each
// entry is a factory: it takes the arguments the JSON gives it and the
// table's URL prefixes, and returns the function DataTables calls.

const ndTables = (function () {
  const esc = function (s) {
    return DataTable.util.escapeHtml(s == null ? '' : String(s));
  };
  const enc = encodeURIComponent;
  const get = function (row, dotted) {
    return String(dotted)
      .split('.')
      .reduce(function (o, k) {
        return o == null ? o : o[k];
      }, row);
  };
  const link = function (href, text) {

share/public/javascripts/netdisco-tables.js  view on Meta::CPAN

     */
    raw: function () {
      return function (data) {
        return data == null ? '' : data;
      };
    },
    /**
     * Escapes the cell value as HTML text for every render type, via the vendored text renderer.
     * @returns {Function} the DataTables render function
     */
    escape: function () {
      return byType(DataTable.render.text());
    },
    /**
     * Formats a numeric cell with thousands separators. The vendored formatter bakes in
     * one fixed decimal count per instance, but a column such as devicepoestatus's
     * wattage readings mixes whole watts with one-decimal values row by row, so the
     * precision is read off each value's own string form instead of fixed at build
     * time. Non-numeric and null values pass through unformatted either way, by the
     * vendored formatter's own guard.
     * @param {object} args args.precision, when given, forces that precision for every row instead of reading it per value

share/public/javascripts/netdisco-tables.js  view on Meta::CPAN

     * @returns {Function} the DataTables render function
     */
    dateTime: function () {
      return function (data, type) {
        if (data == null) return '';
        const text = String(data).replace('T', ' ');
        return type === 'display' || type === 'filter' ? esc(text.slice(0, 16)) : text;
      };
    },
    /**
     * Upper-cases the first character of the escaped cell text, leaving the rest unchanged.
     * @returns {Function} the DataTables render function
     */
    capitalize: function () {
      return function (data) {
        const s = data == null ? '' : String(data);
        return esc(s.charAt(0).toUpperCase() + s.slice(1));
      };
    },
    /**
     * Joins the row array named by args.key into one string, escaping each element.
     * @param {object} args args.key names the row array to join; args.with overrides the default "<br />" separator
     * @returns {Function} the DataTables render function
     */
    join: function (args) {
      if (!args.key) throw new Error('renderer join needs a "key" naming the row array to join');
      return function (data, type, row) {
        return (get(row, args.key) || []).map(esc).join(args.with || '<br />');
      };
    },
    /**
     * Renders the row's device label (dns, else name, else ip) as escaped plain text, with no link.
     * @param {object} args the dns/name/ip label selection, as read by deviceLabel
     * @returns {Function} the DataTables render function
     */
    deviceName: function (args) {
      return function (data, type, row) {
        return esc(deviceLabel(args, row, data));
      };
    },

    /**
     * Links to a device's ports tab. Sorting and filtering see only the escaped cell
     * value, unless args.always asks for the link on every render type as the closures
     * this replaced did.
     * @param {object} args args.q and args.f each name a row key, or "data" for the cell
     *   itself: q for the device address, f for the port. args.qFallback names a row
     *   key used for q when that key's own value is empty. args.flags is a literal
     *   query suffix; args.descr adds a second line from a row key; args.class sets the
     *   anchor's classes; args.text names a row key shown instead of the cell;
     *   args.number formats the shown text with thousands separators; args.always
     *   renders the link for every render type
     * @param {{[key: string]: string}} urls the URL prefixes from data-nd-urls; reads "device_ports"

share/public/javascripts/netdisco-tables.js  view on Meta::CPAN

      const searchDeviceUrl = urlFor(urls, 'search_device', 'searchDeviceLink');
      return function (data, type, row) {
        if ((data == null || data === '') && args.notSet) return args.notSet;
        const href = searchDeviceUrl + '&q=' + enc(data) + (args.also ? '&' + args.also + '=' + enc(data) : '');
        const text = esc(args.label ? deviceLabel(args.label, row, data) : data);
        return link(href, text);
      };
    },

    /**
     * Links to the node search. Sort and filter see the escaped cell text alone unless
     * args.always builds the link for every render type; the cell is also shown plain
     * (no link) whenever it is empty or, when args.onlyIf is given, that row key is falsy.
     * @param {object} args args.upper shows the MAC upper-cased; args.archived adds the
     *   archived flag and marker for an inactive row; args.mark appends the marker
     *   inside the link without the flag; args.domainPrefix prepends the NetBIOS
     *   domain; args.onlyIf names a row key that must be truthy for the link to render
     *   at all; args.always renders the link for every render type, not just display
     * @param {{[key: string]: string}} urls the URL prefixes from data-nd-urls; reads "search_node"
     * @returns {Function} the DataTables render function
     */

share/public/javascripts/netdisco-tables.js  view on Meta::CPAN

        const flag = args.archived && !row.active ? '&archived=on' : '';
        const prefix = args.domainPrefix && row.domain ? esc('\\\\' + row.domain + '\\') : '';
        return prefix + link(searchNodeUrl + '&q=' + enc(data) + flag, shown + archivedMark(args, row));
      };
    },

    /**
     * report/ipinventory ip column: for the display render type, a node link when the
     * address belongs to a known node, a device link when it was only seen on a
     * device, plain text when never seen (row.time_last unset); every other render
     * type sees the escaped cell text alone.
     * @param {object} args unused; kept for the renderer factory signature
     * @param {{[key: string]: string}} urls the URL prefixes from data-nd-urls; reads "search_node" and "search_device"
     * @returns {Function} the DataTables render function
     */
    ipInventoryAddress: function (args, urls) {
      // Which URL is needed depends on the row, not the column, so each is
      // resolved only on the branch that actually uses it.
      return function (data, type, row) {
        const text = esc(data);
        if (type !== 'display' || !row.time_last) return text;

share/public/javascripts/netdisco-tables.js  view on Meta::CPAN

  // of the latter alone; the getter keeps groupRows from ever calling
  // api.row() at all.
  const groupHeaders = function (api, colspan, labelFor) {
    const rows = api.rows({ page: 'current' }).nodes();
    let last = null;
    api
      .column(0, { page: 'current' })
      .data()
      .each(function (group, i) {
        if (last !== group) {
          // labelFor escapes its fields, or passes cell HTML the server already escaped
          // eslint-disable-next-line no-unsanitized/method
          rows[i].insertAdjacentHTML(
            'beforebegin',
            '<tr class="group"><td colspan="' +
              colspan +
              '">' +
              labelFor(group, function () {
                return api.row(i).data();
              }) +
              '</td></tr>'

share/public/javascripts/netdisco-tables.js  view on Meta::CPAN

          last = group;
        }
      });
  };

  const CALLBACKS = {
    /**
     * Inserts one group header row above each run of equal first-column values,
     * repeating the first column's own text as the group's label.
     * @param {object} args args.colspan is the visible column count; the group value is
     *   escaped by default, matching a data-nd-data JSON row (raw text), but args.html
     *   true skips escaping for a DOM-sourced table, whose cells DataTables reads as
     *   already-rendered HTML; args.toggleOrder makes a click on a group header flip the
     *   sort direction
     * @returns {Function} the DataTables drawCallback function
     */
    groupRows: function (args) {
      const renderGroup = args.html
        ? function (v) {
            return v;
          }

share/public/javascripts/netdisco-tables.js  view on Meta::CPAN

        groupHeaders(api, args.colspan, function (group) {
          return renderGroup(group);
        });
        if (args.toggleOrder) bindGroupOrderToggle(api);
      };
    },

    /**
     * report/apradiochannelpower and report/devicepoestatus: inserts one group header
     * row above each run of equal first-column values, built as a whole-row device
     * label rather than the escaped value of column 0, since column 0 alone does not
     * carry enough of the row to build it.
     * @param {object} args args.colspan and args.toggleOrder as groupRows; there is no
     *   args.html, since the label is always built from row fields and escaped itself,
     *   never from an already-rendered cell. args.nameKey names the row key read when
     *   dns is absent: report/apradiochannelpower and report/devicepoestatus key it
     *   differently, device_name and name respectively
     * @param {{[key: string]: string}} urls the URL prefixes from data-nd-urls; reads "device"
     * @returns {Function} the DataTables drawCallback function
     */
    groupDeviceRows: function (args, urls) {
      if (!args.nameKey)
        throw new Error('callback groupDeviceRows needs a "nameKey" naming the row key to use when dns is absent');
      const deviceUrl = urlFor(urls, 'device', 'groupDeviceRows');

share/public/javascripts/netdisco-tables.js  view on Meta::CPAN

   * logging and skipping any table whose spec fails to build instead of letting one bad
   * table stop the rest.
   * @param {Element} [root] the just-swapped htmx fragment root, or the whole document when omitted
   * @returns {void}
   */
  function init(root) {
    const tables = (root || document).querySelectorAll('table[data-nd-table]');
    Array.prototype.forEach.call(tables, function (table) {
      if (table.classList.contains('dataTable')) return;
      // One bad data-nd-table (or data-nd-urls) degrades one table, not the
      // whole pane: a throw here would otherwise escape the htmx:after:swap
      // listener and leave holdUntilSettled never called.
      try {
        build(table, root);
      } catch (e) {
        console.error(e);
      }
    });
  }

  return {

share/public/javascripts/netdisco-typeahead.js  view on Meta::CPAN

        return { label: String(row.label), value: String(row.value) };
      }
      return { label: String(row), value: String(row) };
    });
  }

  /**
   * Appends a row's label to a node, wrapping each occurrence of the typed term
   * in a strong element so the list shows why each row is in it. Built from text
   * nodes because the labels are device names out of the database, so nothing
   * here produces markup and nothing has to be escaped.
   * @param {Element} node the element the label is appended to
   * @param {string} label the row's label
   * @param {string} term the text typed so far, which may be empty
   * @returns {void}
   */
  function highlightInto(node, label, term) {
    const doc = node.ownerDocument;
    if (!term) {
      node.appendChild(doc.createTextNode(label));
      return;

share/public/javascripts/netdisco.js  view on Meta::CPAN

// own script builds its table from a ready callback afterwards, so the raw
// full-length table would paint with no indicator until that finishes.
//
// Quiet DOM rather than a table library's own event. Quiet is not enough on
// its own: the build has gaps of several hundred milliseconds where nothing
// changes because the thread is busy computing, and revealing in one of
// those shows a table that is still moving.
// A frame that took far longer than a frame should is the evidence of that, so
// both conditions have to hold, twice running.
//
// The deadline is an escape hatch for a pane that never goes quiet rather than
// a budget: it starts at the swap, so it covers only the browser's own work,
// never the fetch.
function holdUntilSettled(pane, indicator) {
  if (!pane) return;
  // This is the only thing that reveals a pane, so declining to hold one cannot
  // mean leaving it hidden: whatever hid it is waiting on this.
  if (!indicator) { pane.classList.remove('nd_pane-settling'); return }
  pane.classList.add('nd_pane-settling');
  indicator.classList.add('nd_indicator-held');

share/public/swagger-ui/swagger-ui-bundle.js  view on Meta::CPAN

/*! For license information please see swagger-ui-bundle.js.LICENSE.txt */
!function webpackUniversalModuleDefinition(s,o){"object"==typeof exports&&"object"==typeof module?module.exports=o():"function"==typeof define&&define.amd?define([],o):"object"==typeof exports?exports.SwaggerUIBundle=o():s.SwaggerUIBundle=o()}(this,(...

share/public/swagger-ui/swagger-ui-bundle.js.map  view on Meta::CPAN

{"version":3,"file":"swagger-ui-bundle.js","mappings":";CAAA,SAAUA,iCAAiCC,EAAMC,GAC1B,iBAAZC,SAA0C,iBAAXC,OACxCA,OAAOD,QAAUD,IACQ,mBAAXG,QAAyBA,OAAOC,IAC9CD,OAAO,GAAIH,GACe,iBAAZC,QACdA,QAAyB,gBAAID,IAE7BD,EAAsB,gBAAIC,GAC3B,CATD,CASGK,MAAM,cCRLC,EA...

share/public/swagger-ui/swagger-ui-standalone-preset.js  view on Meta::CPAN

/*! For license information please see swagger-ui-standalone-preset.js.LICENSE.txt */
!function webpackUniversalModuleDefinition(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.SwaggerUIStandalonePreset=e():t.SwaggerUIStandalo...

share/public/swagger-ui/swagger-ui-standalone-preset.js.map  view on Meta::CPAN

{"version":3,"file":"swagger-ui-standalone-preset.js","mappings":";CAAA,SAAUA,iCAAiCC,EAAMC,GAC1B,iBAAZC,SAA0C,iBAAXC,OACxCA,OAAOD,QAAUD,IACQ,mBAAXG,QAAyBA,OAAOC,IAC9CD,OAAO,GAAIH,GACe,iBAAZC,QACdA,QAAmC,0BAAID,IAEvCD,EAAgC,0BAAIC,GACrC,CATD,CASGK,MA...

share/views/ajax/admintask/userlog.tt  view on Meta::CPAN

[% IF results.count == 0 %]
<div class="col-md-2 alert alert-info">The user activity log is empty.</div>
[% ELSE %]
<table id="aul-data-table" class="table table-bordered table-sm table-hover" width="100%" cellspacing="0"
  data-nd-table='{
  "serverSide": true,
  "order": [[0,"desc"]],
  "ajax": "[% uri_for("/ajax/control/admin/userlog/data") | html_entity %]",
  "columns": [
    {"data":"creation","className":"nd_center-cell","render":"dateTime"},
    {"data":"username","className":"nd_center-cell","render":"escape"},
    {"data":"userip","className":"nd_center-cell","render":"escape"},
    {"data":"event","className":"nd_center-cell","render":"escape"}
  ]
}'>
  <thead>
    <tr>
      <th class="nd_center-cell">Creation</th>
      <th class="nd_center-cell">User</th>
      <th class="nd_center-cell">User IP</th>
      <th class="nd_center-cell">Event</th>
    </tr>
  </thead>

share/views/ajax/device/addresses.tt  view on Meta::CPAN

  data-nd-urls='{
  "device_ports": "[% device_ports | html_entity %]",
  "search_device": "[% search_device | html_entity %]"
  [% IF user_has_role('admin') %]
  ,"uri_base": "[% uri_base | html_entity %]"
  [% END %]
}'
  data-nd-table='{
  "deferRender": true,
  "columns": [
    {"data":"alias","render":"escape"},
    {"data":"dns","render":"escape"},
    {"data":"port","type":"portsort","render":{"name":"devicePortsLink","q":"ip","f":"data"}},
    {"data":"device_port.name","render":"escape"},
    {"data":"subnet","render":"subnetLink"}
  ]
}'>
  <thead>
    <tr>
      <th>Address</th>
      <th>DNS</th>
[%# This header alone asked to be centered while its own column's data is left
    aligned, so it sat out of line with both its data and its neighbors. The
    class is dropped rather than the data centered, because every other column in

share/views/ajax/report/apchanneldist.tt  view on Meta::CPAN

<script type="application/json" id="nd-results-report-apchanneldist">[% results.replace('<', '\u003c') | none %]</script>
<table id="data-table" class="table table-striped table-bordered" width="100%" cellspacing="0"
  data-nd-data="#nd-results-report-apchanneldist"
  data-nd-table='{"deferRender":true,"order":[[1,"desc"]],"columns":[{"data":"channel","render":"escape"},{"data":"ch_count","searchable":false,"render":"number"}]}'>
  <thead>
    <tr>
      <th>Channel</th>
      <th>Count</th>
    </tr>
  </thead>
</table>

share/views/ajax/report/apclients.tt  view on Meta::CPAN

  data-nd-data="#nd-results-report-apclients"
  data-nd-urls='{"device_ports":"[% device_ports | html_entity %]"}'
  data-nd-table='{
  "deferRender": true,
  "columns": [
    {
      "data": "ports.port",
      "type": "portsort",
      "render": {"name":"devicePortsLink","q":"ip","f":"data","flags":"c_nodes=on","text":"ports.name"}
    },
    {"data":"model","render":"escape"},
    {"data":"ports.type","render":"escape"},
    {"data":"mac_count","searchable":false,"render":"number"}
  ]
}'>
  <thead>
    <tr>
      <th>Access Point</th>
      <th>Model</th>
      <th>Radio</th>
      <th>Node Count</th>
    </tr>

share/views/ajax/report/apradiochannelpower.tt  view on Meta::CPAN

  data-nd-urls='{"device_ports":"[% device_ports | html_entity %]","device":"[% uri_for('/device') | html_entity %]"}'
  data-nd-table='{
  "serverSide": true,
  "ajax": "[% uri_for("/ajax/content/report/apradiochannelpower/data") | html_entity %]",
  "order": [[0,"asc"]],
  "columns": [
    {"data":"ip","visible":false},
    {"data":"dns","visible":false},
    {"data":"device_name","visible":false},
    {"data":"port","type":"portsort","render":{"name":"devicePortsLink","q":"ip","f":"data"}},
    {"data":"port_name","render":"escape"},
    {"data":"descr","render":"escape"},
    {"data":"channel","className":"nd_center-cell","render":"escape"},
    {"data":"power","className":"nd_center-cell","render":"powerPair"}
  ],
  "drawCallback": {"name":"groupDeviceRows","colspan":5,"nameKey":"device_name","toggleOrder":true}
}'>
  <thead>
    <tr>
      <th>IP</th>
      <th>DNS</th>
      <th>Device</th>
      <th>Port</th>

share/views/ajax/report/deviceaddrnodns.tt  view on Meta::CPAN

<script type="application/json" id="nd-results-report-deviceaddrnodns">[% results.replace('<', '\u003c') | none %]</script>
<table id="data-table" class="table table-striped table-bordered" width="100%" cellspacing="0"
  data-nd-data="#nd-results-report-deviceaddrnodns"
  data-nd-urls='{"search_device":"[% search_device | html_entity %]"}'
  data-nd-table='{
  "deferRender": true,
  "columns": [
    {"data":"ip","render":{"name":"searchDeviceLink","label":{"dns":"dns","name":"name","ip":"ip"}}},
    {"data":"alias","render":"escape"},
    {"data":"contact","render":"escape"},
    {"data":"location","render":"escape"}
  ]
}'>
  <thead>
    <tr>
      <th class="nd_center-cell">Device</th>
      <th class="nd_center-cell">Address</th>
      <th class="nd_center-cell">Contact</th>
      <th class="nd_center-cell">Location</th>
    </tr>
  </thead>

share/views/ajax/report/devicednsmismatch.tt  view on Meta::CPAN

<script type="application/json" id="nd-results-report-devicednsmismatch">[% results.replace('<', '\u003c') | none %]</script>
<table id="data-table" class="table table-striped table-bordered" width="100%" cellspacing="0"
  data-nd-data="#nd-results-report-devicednsmismatch"
  data-nd-urls='{"search_device":"[% search_device | html_entity %]"}'
  data-nd-table='{
  "deferRender": true,
  "columns": [
    {"data":"ip","render":{"name":"searchDeviceLink","label":{"name":"name","ip":"ip"}}},
    {"data":"dns","render":"escape"},
    {"data":"ip","render":"escape"},
    {"data":"contact","render":"escape"},
    {"data":"location","render":"escape"}
  ]
}'>
  <thead>
    <tr>
      <th>Name</th>
      <th>DNS</th>
      <th>IP Address</th>
      <th>Contact</th>
      <th>Location</th>
    </tr>

share/views/ajax/report/devicepoestatus.tt  view on Meta::CPAN

  data-nd-table='{
  "serverSide": true,
  "ajax": "[% uri_for("/ajax/content/report/devicepoestatus/data") | html_entity %]",
  "order": [[0,"asc"]],
  "columns": [
    {"data":"ip","visible":false},
    {"data":"dns","visible":false},
    {"data":"name","visible":false},
    {"data":"model","visible":false},
    {"data":"location","visible":false},
    {"data":"module","searchable":false,"render":"escape"},
    {"data":"status","className":"nd_center-cell","render":"escape"},
    {"data":"power","className":"nd_center-cell","searchable":false,"render":"number"},
    {"data":"poe_capable_ports","className":"nd_center-cell","searchable":false,"render":"number"},
    {"data":"poe_powered_ports","className":"nd_center-cell","searchable":false,"render":"number"},
    {"data":"poe_disabled_ports","className":"nd_center-cell","searchable":false,"render":"number"},
    {"data":"poe_errored_ports","className":"nd_center-cell","searchable":false,"render":"number"},
    {"data":"poe_power_committed","className":"nd_center-cell","searchable":false,"render":"number"},
    {"data":"poe_power_delivering","className":"nd_center-cell","searchable":false,"render":"number"}
  ],
  "drawCallback": {"name":"groupDeviceRows","colspan":9,"nameKey":"name","toggleOrder":true}
}'>

share/views/ajax/report/halfduplex.tt  view on Meta::CPAN

  data-nd-urls='{"device_ports":"[% device_ports | html_entity %]"}'
  data-nd-table='{
  "deferRender": true,
  "columns": [
    {"data":"ip","render":{"name":"deviceName","label":{"dns":"device.dns","name":"device.name","ip":"ip"}}},
    {
      "data": "port",
      "type": "portsort",
      "render": {"name":"devicePortsLink","q":"ip","f":"data","flags":"c_duplex=on"}
    },
    {"data":"name","render":"escape"},
    {"data":"duplex","searchable":false,"orderable":false,"render":"capitalize"}
  ]
}'>
  <thead>
    <tr>
      <th>Device</th>
      <th>Port</th>
      <th>Description</th>
      <th>Duplex</th>
    </tr>

share/views/ajax/report/ipinventory.tt  view on Meta::CPAN

<script type="application/json" id="nd-results-report-ipinventory">[% results.replace('<', '\u003c') | none %]</script>
<table id="data-table" class="table table-striped table-bordered" width="100%" cellspacing="0"
  data-nd-data="#nd-results-report-ipinventory"
  data-nd-urls='{"search_node":"[% search_node | html_entity %]","search_device":"[% search_device | html_entity %]"}'
  data-nd-table='{
  "deferRender": true,
  "columns": [
    {"data":"ip","render":"ipInventoryAddress"},
    {"data":"mac","render":{"name":"searchNodeLink","archived":true,"onlyIf":"time_last"}},
    {"data":"vendor","className":"nd_nowrap nd_center-cell","render":"escape"},
    {"data":"dns","className":"nd_nowrap nd_center-cell","render":"escape"},
    {"data":"nbname","className":"nd_nowrap nd_center-cell","render":"escape"},
    {"data":"age","render":{"name":"age","stamp":"time_last"}}
  ]
}'>
  <thead>
    <tr>
      <th>IP Address</th>
      <th>MAC Address</th>
      <th>Vendor</th>
      <th class="nd_center-cell">DNS</th>
      <th class="nd_center-cell">NetBIOS</th>



( run in 2.180 seconds using v1.01-cache-2.11-cpan-54e63673c56 )