From 1f2cff461b8e8eb011daf6a7c8b27e177b4b3a39 Mon Sep 17 00:00:00 2001 From: dparkerelastic Date: Tue, 17 Sep 2024 16:39:28 -0400 Subject: [PATCH 1/2] x-pack/metricbeat/module/panw: Add a new module (#40686) * initial module creation * panos.system metricset running * remove testing data * panos.disk metricset working * rename metricset * change metricset name * bgp_peers metricset working * temperature metricset * more metricsets * use MetricSetFields * license notices * update fields.yml * added doc * refactor down to 4 metricsets * more cleanup * cleanup field names * remove yml * panos.yml.disabled * PR comment fixes * more PR comments addressed. Still to do: tests * Changes to: - move tunnels from vpn to interfaces metricset - address PR comments for field names in field.yml - split local/peer addresses into host and port for bgp - handle license expires of "never" * Fixes for PR comments * add license header * add pango package * mage check && mage update * remove mappings & make update * make linter happy * add the untracked docs * update the fields.yml * update the fields.yml with example fields to make python integ tests happy * make docs check happy and update codeowners * add result of 'mage update' in x-pack/metricbeat --------- Co-authored-by: subham sarkar Co-authored-by: tommyers-elastic <106530686+tommyers-elastic@users.noreply.github.com> (cherry picked from commit cc2c92571f320a68589eda39e0f95d859d101e21) # Conflicts: # go.mod # x-pack/metricbeat/metricbeat.reference.yml --- .github/CODEOWNERS | 1 + NOTICE.txt | 25 ++ go.mod | 5 + go.sum | 2 + metricbeat/docs/fields.asciidoc | 45 +++ metricbeat/docs/modules/panw.asciidoc | 143 ++++++++ .../docs/modules/panw/interfaces.asciidoc | 29 ++ metricbeat/docs/modules/panw/routing.asciidoc | 29 ++ metricbeat/docs/modules/panw/system.asciidoc | 29 ++ metricbeat/docs/modules/panw/vpn.asciidoc | 29 ++ metricbeat/docs/modules_list.asciidoc | 6 + x-pack/metricbeat/include/list.go | 5 + x-pack/metricbeat/metricbeat.reference.yml | 72 ++++ .../metricbeat/module/panw/_meta/config.yml | 6 + .../module/panw/_meta/docs.asciidoc | 86 +++++ .../metricbeat/module/panw/_meta/fields.yml | 9 + .../module/panw/_meta/testdata/bgp_peers.xml | 337 ++++++++++++++++++ x-pack/metricbeat/module/panw/client.go | 50 +++ x-pack/metricbeat/module/panw/config.go | 30 ++ x-pack/metricbeat/module/panw/doc.go | 6 + x-pack/metricbeat/module/panw/fields.go | 23 ++ .../module/panw/interfaces/_meta/data.json | 19 + .../panw/interfaces/_meta/docs.asciidoc | 1 + .../module/panw/interfaces/_meta/fields.yml | 7 + .../module/panw/interfaces/ha_interfaces.go | 183 ++++++++++ .../panw/interfaces/ifnet_interfaces.go | 115 ++++++ .../module/panw/interfaces/interface_types.go | 249 +++++++++++++ .../module/panw/interfaces/interfaces.go | 101 ++++++ .../module/panw/interfaces/tunnels.go | 81 +++++ .../module/panw/routing/_meta/data.json | 19 + .../module/panw/routing/_meta/docs.asciidoc | 1 + .../module/panw/routing/_meta/fields.yml | 7 + .../module/panw/routing/bgp_peers.go | 140 ++++++++ .../metricbeat/module/panw/routing/routing.go | 98 +++++ .../module/panw/routing/routing_types.go | 83 +++++ .../module/panw/system/_meta/data.json | 19 + .../module/panw/system/_meta/docs.asciidoc | 1 + .../module/panw/system/_meta/fields.yml | 7 + .../module/panw/system/certificates.go | 149 ++++++++ x-pack/metricbeat/module/panw/system/fans.go | 68 ++++ .../module/panw/system/filesystem.go | 156 ++++++++ .../metricbeat/module/panw/system/license.go | 101 ++++++ x-pack/metricbeat/module/panw/system/power.go | 69 ++++ .../module/panw/system/resources.go | 329 +++++++++++++++++ .../metricbeat/module/panw/system/system.go | 106 ++++++ .../module/panw/system/system_types.go | 212 +++++++++++ .../metricbeat/module/panw/system/thermal.go | 76 ++++ x-pack/metricbeat/module/panw/util.go | 38 ++ .../module/panw/vpn/_meta/data.json | 19 + .../module/panw/vpn/_meta/docs.asciidoc | 1 + .../module/panw/vpn/_meta/fields.yml | 7 + .../metricbeat/module/panw/vpn/gp_sessions.go | 89 +++++ x-pack/metricbeat/module/panw/vpn/gp_stats.go | 71 ++++ x-pack/metricbeat/module/panw/vpn/vpn.go | 101 ++++++ .../metricbeat/module/panw/vpn/vpn_types.go | 61 ++++ x-pack/metricbeat/modules.d/panw.yml.disabled | 9 + 56 files changed, 3760 insertions(+) create mode 100644 metricbeat/docs/modules/panw.asciidoc create mode 100644 metricbeat/docs/modules/panw/interfaces.asciidoc create mode 100644 metricbeat/docs/modules/panw/routing.asciidoc create mode 100644 metricbeat/docs/modules/panw/system.asciidoc create mode 100644 metricbeat/docs/modules/panw/vpn.asciidoc create mode 100644 x-pack/metricbeat/module/panw/_meta/config.yml create mode 100644 x-pack/metricbeat/module/panw/_meta/docs.asciidoc create mode 100644 x-pack/metricbeat/module/panw/_meta/fields.yml create mode 100644 x-pack/metricbeat/module/panw/_meta/testdata/bgp_peers.xml create mode 100644 x-pack/metricbeat/module/panw/client.go create mode 100644 x-pack/metricbeat/module/panw/config.go create mode 100644 x-pack/metricbeat/module/panw/doc.go create mode 100644 x-pack/metricbeat/module/panw/fields.go create mode 100644 x-pack/metricbeat/module/panw/interfaces/_meta/data.json create mode 100644 x-pack/metricbeat/module/panw/interfaces/_meta/docs.asciidoc create mode 100644 x-pack/metricbeat/module/panw/interfaces/_meta/fields.yml create mode 100644 x-pack/metricbeat/module/panw/interfaces/ha_interfaces.go create mode 100644 x-pack/metricbeat/module/panw/interfaces/ifnet_interfaces.go create mode 100644 x-pack/metricbeat/module/panw/interfaces/interface_types.go create mode 100644 x-pack/metricbeat/module/panw/interfaces/interfaces.go create mode 100644 x-pack/metricbeat/module/panw/interfaces/tunnels.go create mode 100644 x-pack/metricbeat/module/panw/routing/_meta/data.json create mode 100644 x-pack/metricbeat/module/panw/routing/_meta/docs.asciidoc create mode 100644 x-pack/metricbeat/module/panw/routing/_meta/fields.yml create mode 100644 x-pack/metricbeat/module/panw/routing/bgp_peers.go create mode 100644 x-pack/metricbeat/module/panw/routing/routing.go create mode 100644 x-pack/metricbeat/module/panw/routing/routing_types.go create mode 100644 x-pack/metricbeat/module/panw/system/_meta/data.json create mode 100644 x-pack/metricbeat/module/panw/system/_meta/docs.asciidoc create mode 100644 x-pack/metricbeat/module/panw/system/_meta/fields.yml create mode 100644 x-pack/metricbeat/module/panw/system/certificates.go create mode 100644 x-pack/metricbeat/module/panw/system/fans.go create mode 100644 x-pack/metricbeat/module/panw/system/filesystem.go create mode 100644 x-pack/metricbeat/module/panw/system/license.go create mode 100644 x-pack/metricbeat/module/panw/system/power.go create mode 100644 x-pack/metricbeat/module/panw/system/resources.go create mode 100644 x-pack/metricbeat/module/panw/system/system.go create mode 100644 x-pack/metricbeat/module/panw/system/system_types.go create mode 100644 x-pack/metricbeat/module/panw/system/thermal.go create mode 100644 x-pack/metricbeat/module/panw/util.go create mode 100644 x-pack/metricbeat/module/panw/vpn/_meta/data.json create mode 100644 x-pack/metricbeat/module/panw/vpn/_meta/docs.asciidoc create mode 100644 x-pack/metricbeat/module/panw/vpn/_meta/fields.yml create mode 100644 x-pack/metricbeat/module/panw/vpn/gp_sessions.go create mode 100644 x-pack/metricbeat/module/panw/vpn/gp_stats.go create mode 100644 x-pack/metricbeat/module/panw/vpn/vpn.go create mode 100644 x-pack/metricbeat/module/panw/vpn/vpn_types.go create mode 100644 x-pack/metricbeat/modules.d/panw.yml.disabled diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8661f59c509..29c37dd36d7 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -219,6 +219,7 @@ CHANGELOG* /x-pack/metricbeat/module/istio/ @elastic/obs-cloudnative-monitoring /x-pack/metricbeat/module/mssql @elastic/obs-infraobs-integrations /x-pack/metricbeat/module/oracle @elastic/obs-infraobs-integrations +/x-pack/metricbeat/module/panw @elastic/obs-infraobs-integrations /x-pack/metricbeat/module/prometheus/ @elastic/obs-cloudnative-monitoring /x-pack/metricbeat/module/redisenterprise @elastic/obs-infraobs-integrations /x-pack/metricbeat/module/sql @elastic/obs-infraobs-integrations diff --git a/NOTICE.txt b/NOTICE.txt index 2a0eaebacfd..4b96b2b198f 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -2720,6 +2720,31 @@ Redistribution and use in source and binary forms, with or without modification, THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. +-------------------------------------------------------------------------------- +Dependency : github.com/PaloAltoNetworks/pango +Version: v0.10.2 +Licence type (autodetected): ISC +-------------------------------------------------------------------------------- + +Contents of probable licence file $GOMODCACHE/github.com/!palo!alto!networks/pango@v0.10.2/LICENSE: + +Distributed under ISC license: + +Copyright (c) 2014-2016, Palo Alto Networks Inc. + +Permission to use, copy, modify, and/or distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY +SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION +OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN +CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + + -------------------------------------------------------------------------------- Dependency : github.com/elastic/sarama Version: v1.19.1-0.20220310193331-ebc2b0d8eef3 diff --git a/go.mod b/go.mod index 235388ea14e..79ea2b827ba 100644 --- a/go.mod +++ b/go.mod @@ -396,7 +396,12 @@ require ( ) require ( +<<<<<<< HEAD cloud.google.com/go/storage v1.38.0 +======= + cloud.google.com/go/storage v1.43.0 + github.com/PaloAltoNetworks/pango v0.10.2 +>>>>>>> cc2c92571f (x-pack/metricbeat/module/panw: Add a new module (#40686)) github.com/dlclark/regexp2 v1.4.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/moby/term v0.5.0 // indirect diff --git a/go.sum b/go.sum index c0a26b49e83..d020b13e072 100644 --- a/go.sum +++ b/go.sum @@ -212,6 +212,8 @@ github.com/PaesslerAG/gval v1.2.2/go.mod h1:XRFLwvmkTEdYziLdaCeCa5ImcGVrfQbeNUbV github.com/PaesslerAG/jsonpath v0.1.0/go.mod h1:4BzmtoM/PI8fPO4aQGIusjGxGir2BzcV0grWtFzq1Y8= github.com/PaesslerAG/jsonpath v0.1.1 h1:c1/AToHQMVsduPAa4Vh6xp2U0evy4t8SWp8imEsylIk= github.com/PaesslerAG/jsonpath v0.1.1/go.mod h1:lVboNxFGal/VwW6d9JzIy56bUsYAP6tH/x80vjnCseY= +github.com/PaloAltoNetworks/pango v0.10.2 h1:Tjn6vIzzAq6Dd7N0mDuiP8w8pz8k5W9zz/TTSUQCsQY= +github.com/PaloAltoNetworks/pango v0.10.2/go.mod h1:GztcRnVLur7G+VFG7Z5ZKNFgScLtsycwPMp1qVebE5g= github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= diff --git a/metricbeat/docs/fields.asciidoc b/metricbeat/docs/fields.asciidoc index e0aeab6e4d3..db5e40de517 100644 --- a/metricbeat/docs/fields.asciidoc +++ b/metricbeat/docs/fields.asciidoc @@ -69,6 +69,7 @@ grouped in the following categories: * <> * <> * <> +* <> * <> * <> * <> @@ -57152,6 +57153,50 @@ format: bytes -- +[[exported-fields-panw]] +== Panw fields + +PAN-OS module + + +[float] +=== panw + +PAN-OS module + + + +*`panw.interfaces.example`*:: ++ +-- +type: keyword + +-- + + +*`panw.routing.example`*:: ++ +-- +type: keyword + +-- + + +*`panw.system.example`*:: ++ +-- +type: keyword + +-- + + +*`panw.vpn.example`*:: ++ +-- +type: keyword + +-- + [[exported-fields-php_fpm]] == PHP_FPM fields diff --git a/metricbeat/docs/modules/panw.asciidoc b/metricbeat/docs/modules/panw.asciidoc new file mode 100644 index 00000000000..c0166416501 --- /dev/null +++ b/metricbeat/docs/modules/panw.asciidoc @@ -0,0 +1,143 @@ +//// +This file is generated! See scripts/mage/docs_collector.go +//// + +:modulename: panw +:edit_url: https://github.com/elastic/beats/edit/main/x-pack/metricbeat/module/panw/_meta/docs.asciidoc + + +[[metricbeat-module-panw]] +[role="xpack"] +== Panw module + +beta[] + +:modulename: panw + +include::{libbeat-dir}/shared/integration-link.asciidoc[] + +:modulename!: + +The panw Metricbeat module uses the Palo Alto [pango](https://pkg.go.dev/github.com/PaloAltoNetworks/pango#section-documentation) package to extract metrics +information from a firewall device via the XML API. + +[float] +=== Dashboards + + +[float] +=== Module-specific configuration notes + +The panw module configuration requires the ip address of the target firewall device and an API Key generated from that firewall. It is assumed +that network access to the firewall is available. All access by the panw module is read-only. + +***Limitations*** +The current version of the module is configured to run against **exactly 1** firewall. Multiple firewalls will require multiple agent configurations. +The module has also not been tested with Panorama, though it should work since it only relies on lower level Client.Op calls to send XML API commands +to the server. + +Required credentials for the `panw` module: + +`host_ip` :: IP address of the firewall - must be network accessible. + +`apiKey`:: An API Key generated via an XML API call to the firewall or via the management dashboard. This + + +[float] +== Metricsets + +[float] +=== `bgp_peers` +This metricset reports information on BGP Peers defined in the firewall. + +[float] +=== `certificates` +This metricset will capture certificates defined on the firewall including expiration dates. + +[float] +=== `fans` +This metricset will collect information from hardware fans (RPMS) and will report if an alarm is active for a given fan. + +[float] +=== `filesystem` +This metricset reports disk usage for filesystems defined on the device, based on df output. + +[float] +=== `globalprotect_sessions` +This metricset will collect metrics on current user sessions established on Global Protect gateways. + +[float] +=== `globalprotect_stats` +This metricset reports the number of user per GlobalProtect gateway and totals across all gateways. + +[float] +=== `ha_interfaces` +This metricset will collect metrics from the device on High Availabilty configuration for interfaces. + +[float] +=== `licenses` +This metricset reports on licenses for sofware features with expiration dates. + +[float] +=== `logical` +This metricset will collect metrics on logical interfaces in the device's network. + +[float] +=== `power` +This metricset reports power usage and alarms. + +[float] +=== `system` +This metricset captures system informate such as uptime, user count, CPU, memory and swap: essentiallyl the first 5 lines of 'top' output. + +[float] +=== `temperature` +This metricset reports temperature for various slots on the device and reports on alarm status. + +[float] +=== `tunnels` +This metricset enumerates ipsec tunnels and their status. + + + +:edit_url: + +[float] +=== Example configuration + +The Panw module supports the standard configuration options that are described +in <>. Here is an example configuration: + +[source,yaml] +---- +metricbeat.modules: +- module: panw + metricsets: ["licenses"] + enabled: false + period: 10s + hosts: ["localhost"] + +---- + +[float] +=== Metricsets + +The following metricsets are available: + +* <> + +* <> + +* <> + +* <> + +include::panw/interfaces.asciidoc[] + +include::panw/routing.asciidoc[] + +include::panw/system.asciidoc[] + +include::panw/vpn.asciidoc[] + +:edit_url!: diff --git a/metricbeat/docs/modules/panw/interfaces.asciidoc b/metricbeat/docs/modules/panw/interfaces.asciidoc new file mode 100644 index 00000000000..6012964a55c --- /dev/null +++ b/metricbeat/docs/modules/panw/interfaces.asciidoc @@ -0,0 +1,29 @@ +//// +This file is generated! See scripts/mage/docs_collector.go +//// +:edit_url: https://github.com/elastic/beats/edit/main/x-pack/metricbeat/module/panw/interfaces/_meta/docs.asciidoc + + +[[metricbeat-metricset-panw-interfaces]] +[role="xpack"] +=== Panw interfaces metricset + +beta[] + +include::../../../../x-pack/metricbeat/module/panw/interfaces/_meta/docs.asciidoc[] + + +:edit_url: + +==== Fields + +For a description of each field in the metricset, see the +<> section. + +Here is an example document generated by this metricset: + +[source,json] +---- +include::../../../../x-pack/metricbeat/module/panw/interfaces/_meta/data.json[] +---- +:edit_url!: \ No newline at end of file diff --git a/metricbeat/docs/modules/panw/routing.asciidoc b/metricbeat/docs/modules/panw/routing.asciidoc new file mode 100644 index 00000000000..3bcaf07fa40 --- /dev/null +++ b/metricbeat/docs/modules/panw/routing.asciidoc @@ -0,0 +1,29 @@ +//// +This file is generated! See scripts/mage/docs_collector.go +//// +:edit_url: https://github.com/elastic/beats/edit/main/x-pack/metricbeat/module/panw/routing/_meta/docs.asciidoc + + +[[metricbeat-metricset-panw-routing]] +[role="xpack"] +=== Panw routing metricset + +beta[] + +include::../../../../x-pack/metricbeat/module/panw/routing/_meta/docs.asciidoc[] + + +:edit_url: + +==== Fields + +For a description of each field in the metricset, see the +<> section. + +Here is an example document generated by this metricset: + +[source,json] +---- +include::../../../../x-pack/metricbeat/module/panw/routing/_meta/data.json[] +---- +:edit_url!: \ No newline at end of file diff --git a/metricbeat/docs/modules/panw/system.asciidoc b/metricbeat/docs/modules/panw/system.asciidoc new file mode 100644 index 00000000000..ad159dffe42 --- /dev/null +++ b/metricbeat/docs/modules/panw/system.asciidoc @@ -0,0 +1,29 @@ +//// +This file is generated! See scripts/mage/docs_collector.go +//// +:edit_url: https://github.com/elastic/beats/edit/main/x-pack/metricbeat/module/panw/system/_meta/docs.asciidoc + + +[[metricbeat-metricset-panw-system]] +[role="xpack"] +=== Panw system metricset + +beta[] + +include::../../../../x-pack/metricbeat/module/panw/system/_meta/docs.asciidoc[] + + +:edit_url: + +==== Fields + +For a description of each field in the metricset, see the +<> section. + +Here is an example document generated by this metricset: + +[source,json] +---- +include::../../../../x-pack/metricbeat/module/panw/system/_meta/data.json[] +---- +:edit_url!: \ No newline at end of file diff --git a/metricbeat/docs/modules/panw/vpn.asciidoc b/metricbeat/docs/modules/panw/vpn.asciidoc new file mode 100644 index 00000000000..b1d9e9df0dd --- /dev/null +++ b/metricbeat/docs/modules/panw/vpn.asciidoc @@ -0,0 +1,29 @@ +//// +This file is generated! See scripts/mage/docs_collector.go +//// +:edit_url: https://github.com/elastic/beats/edit/main/x-pack/metricbeat/module/panw/vpn/_meta/docs.asciidoc + + +[[metricbeat-metricset-panw-vpn]] +[role="xpack"] +=== Panw vpn metricset + +beta[] + +include::../../../../x-pack/metricbeat/module/panw/vpn/_meta/docs.asciidoc[] + + +:edit_url: + +==== Fields + +For a description of each field in the metricset, see the +<> section. + +Here is an example document generated by this metricset: + +[source,json] +---- +include::../../../../x-pack/metricbeat/module/panw/vpn/_meta/data.json[] +---- +:edit_url!: \ No newline at end of file diff --git a/metricbeat/docs/modules_list.asciidoc b/metricbeat/docs/modules_list.asciidoc index b49cd142721..f68dc8e1e65 100644 --- a/metricbeat/docs/modules_list.asciidoc +++ b/metricbeat/docs/modules_list.asciidoc @@ -246,6 +246,11 @@ This file is generated! See scripts/mage/docs_collector.go .3+| .3+| |<> |<> beta[] |<> +|<> beta[] |image:./images/icon-no.png[No prebuilt dashboards] | +.4+| .4+| |<> beta[] +|<> beta[] +|<> beta[] +|<> beta[] |<> |image:./images/icon-no.png[No prebuilt dashboards] | .2+| .2+| |<> |<> @@ -378,6 +383,7 @@ include::modules/nats.asciidoc[] include::modules/nginx.asciidoc[] include::modules/openmetrics.asciidoc[] include::modules/oracle.asciidoc[] +include::modules/panw.asciidoc[] include::modules/php_fpm.asciidoc[] include::modules/postgresql.asciidoc[] include::modules/prometheus.asciidoc[] diff --git a/x-pack/metricbeat/include/list.go b/x-pack/metricbeat/include/list.go index b13740cfe44..01ce86edf78 100644 --- a/x-pack/metricbeat/include/list.go +++ b/x-pack/metricbeat/include/list.go @@ -57,6 +57,11 @@ import ( _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/oracle/performance" _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/oracle/sysmetric" _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/oracle/tablespace" + _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw/interfaces" + _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw/routing" + _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw/system" + _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw/vpn" _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/prometheus" _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/prometheus/collector" _ "github.com/elastic/beats/v7/x-pack/metricbeat/module/prometheus/remote_write" diff --git a/x-pack/metricbeat/metricbeat.reference.yml b/x-pack/metricbeat/metricbeat.reference.yml index e8ec973a75c..764665119e9 100644 --- a/x-pack/metricbeat/metricbeat.reference.yml +++ b/x-pack/metricbeat/metricbeat.reference.yml @@ -1278,6 +1278,14 @@ metricbeat.modules: # username: "" # password: "" +#--------------------------------- Panw Module --------------------------------- +- module: panw + metricsets: ["licenses"] + enabled: false + period: 10s + hosts: ["localhost"] + + #------------------------------- PHP_FPM Module ------------------------------- - module: php_fpm metricsets: @@ -1319,6 +1327,7 @@ metricbeat.modules: # Password to use when connecting to PostgreSQL. Empty by default. #password: pass +<<<<<<< HEAD #------------------------------ Prometheus Module ------------------------------ # Metrics collected from a Prometheus endpoint - module: prometheus @@ -1374,6 +1383,8 @@ metricbeat.modules: # params: # query: "some_value" +======= +>>>>>>> cc2c92571f (x-pack/metricbeat/module/panw: Add a new module (#40686)) #----------------------- Prometheus Typed Metrics Module ----------------------- - module: prometheus period: 10s @@ -1445,6 +1456,67 @@ metricbeat.modules: # params: # query: "some_value" +#------------------------------ Prometheus Module ------------------------------ +# Metrics collected from a Prometheus endpoint +- module: prometheus + period: 10s + metricsets: ["collector"] + hosts: ["localhost:9090"] + metrics_path: /metrics + #metrics_filters: + # include: [] + # exclude: [] + #username: "user" + #password: "secret" + + # Count number of metrics present in Elasticsearch document (default: false) + #metrics_count: false + + # This can be used for service account based authorization: + #bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token + #ssl.certificate_authorities: + # - /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt + + +# Metrics sent by a Prometheus server using remote_write option +#- module: prometheus +# metricsets: ["remote_write"] +# host: "localhost" +# port: "9201" + + # Count number of metrics present in Elasticsearch document (default: false) + #metrics_count: false + + # Secure settings for the server using TLS/SSL: + #ssl.certificate: "/etc/pki/server/cert.pem" + #ssl.key: "/etc/pki/server/cert.key" + +# Metrics that will be collected using a PromQL +#- module: prometheus +# metricsets: ["query"] +# hosts: ["localhost:9090"] +# period: 10s +# queries: +# - name: "instant_vector" +# path: "/api/v1/query" +# params: +# query: "sum(rate(prometheus_http_requests_total[1m]))" +# - name: "range_vector" +# path: "/api/v1/query_range" +# params: +# query: "up" +# start: "2019-12-20T00:00:00.000Z" +# end: "2019-12-21T00:00:00.000Z" +# step: 1h +# - name: "scalar" +# path: "/api/v1/query" +# params: +# query: "100" +# - name: "string" +# path: "/api/v1/query" +# params: +# query: "some_value" + #------------------------------- RabbitMQ Module ------------------------------- - module: rabbitmq metricsets: ["node", "queue", "connection", "exchange", "shovel"] diff --git a/x-pack/metricbeat/module/panw/_meta/config.yml b/x-pack/metricbeat/module/panw/_meta/config.yml new file mode 100644 index 00000000000..9a68d01f798 --- /dev/null +++ b/x-pack/metricbeat/module/panw/_meta/config.yml @@ -0,0 +1,6 @@ +- module: panw + metricsets: ["licenses"] + enabled: false + period: 10s + hosts: ["localhost"] + diff --git a/x-pack/metricbeat/module/panw/_meta/docs.asciidoc b/x-pack/metricbeat/module/panw/_meta/docs.asciidoc new file mode 100644 index 00000000000..f74449adf0f --- /dev/null +++ b/x-pack/metricbeat/module/panw/_meta/docs.asciidoc @@ -0,0 +1,86 @@ +:modulename: panw + +include::{libbeat-dir}/shared/integration-link.asciidoc[] + +:modulename!: + +The panw Metricbeat module uses the Palo Alto [pango](https://pkg.go.dev/github.com/PaloAltoNetworks/pango#section-documentation) package to extract metrics +information from a firewall device via the XML API. + +[float] +=== Dashboards + + +[float] +=== Module-specific configuration notes + +The panw module configuration requires the ip address of the target firewall device and an API Key generated from that firewall. It is assumed +that network access to the firewall is available. All access by the panw module is read-only. + +***Limitations*** +The current version of the module is configured to run against **exactly 1** firewall. Multiple firewalls will require multiple agent configurations. +The module has also not been tested with Panorama, though it should work since it only relies on lower level Client.Op calls to send XML API commands +to the server. + +Required credentials for the `panw` module: + +`host_ip` :: IP address of the firewall - must be network accessible. + +`apiKey`:: An API Key generated via an XML API call to the firewall or via the management dashboard. This + + +[float] +== Metricsets + +[float] +=== `bgp_peers` +This metricset reports information on BGP Peers defined in the firewall. + +[float] +=== `certificates` +This metricset will capture certificates defined on the firewall including expiration dates. + +[float] +=== `fans` +This metricset will collect information from hardware fans (RPMS) and will report if an alarm is active for a given fan. + +[float] +=== `filesystem` +This metricset reports disk usage for filesystems defined on the device, based on df output. + +[float] +=== `globalprotect_sessions` +This metricset will collect metrics on current user sessions established on Global Protect gateways. + +[float] +=== `globalprotect_stats` +This metricset reports the number of user per GlobalProtect gateway and totals across all gateways. + +[float] +=== `ha_interfaces` +This metricset will collect metrics from the device on High Availabilty configuration for interfaces. + +[float] +=== `licenses` +This metricset reports on licenses for sofware features with expiration dates. + +[float] +=== `logical` +This metricset will collect metrics on logical interfaces in the device's network. + +[float] +=== `power` +This metricset reports power usage and alarms. + +[float] +=== `system` +This metricset captures system informate such as uptime, user count, CPU, memory and swap: essentiallyl the first 5 lines of 'top' output. + +[float] +=== `temperature` +This metricset reports temperature for various slots on the device and reports on alarm status. + +[float] +=== `tunnels` +This metricset enumerates ipsec tunnels and their status. + diff --git a/x-pack/metricbeat/module/panw/_meta/fields.yml b/x-pack/metricbeat/module/panw/_meta/fields.yml new file mode 100644 index 00000000000..18f74261819 --- /dev/null +++ b/x-pack/metricbeat/module/panw/_meta/fields.yml @@ -0,0 +1,9 @@ +- key: panw + title: "Panw" + description: PAN-OS module + release: beta + fields: + - name: panw + type: group + description: PAN-OS module + fields: diff --git a/x-pack/metricbeat/module/panw/_meta/testdata/bgp_peers.xml b/x-pack/metricbeat/module/panw/_meta/testdata/bgp_peers.xml new file mode 100644 index 00000000000..1b975b9ae2d --- /dev/null +++ b/x-pack/metricbeat/module/panw/_meta/testdata/bgp_peers.xml @@ -0,0 +1,337 @@ + + + + POC + 10.100.11.2 + 64711 + Established + 17840221 + no + no + 1 + 1.1.1.1:179 + 1.1.1.1:43896 + not-client + no + yes + Unspecified + 15 + 0 + 15 + 0 + 90 + 90 + 30 + 30 + 7221 + 10242 + 314758 + 361880 + 26 + + 4 + 1 + 0 + no + yes + no + + no + + + + Multiprotocol Extensions(1) + IPv4 Unicast + + + Route Refresh(2) + yes + + + Extended Next Hop Encoding(5) + 000100010002000100800002 + + + Graceful Restart(64) + 007800010100 + + + 4-Byte AS Number(65) + 64711 + + + Deprecated(66) + yes + + + Dynamic Capability(67) + 020140 + + + Route Refresh (Cisco)(128) + yes + + + + + 12 + 12 + 0 + 0 + 78 + 78 + + + + + POC + 1.1.1.1 + 64711 + Established + 17840224 + no + no + 1 + 1.1.1.1:179 + 1.1.1.1:52945 + not-client + no + yes + Unspecified + 15 + 0 + 15 + 0 + 90 + 90 + 30 + 30 + 7244 + 10235 + 314777 + 362006 + 1 + + 3 + 1 + 0 + no + yes + no + + no + + + + Multiprotocol Extensions(1) + IPv4 Unicast + + + Route Refresh(2) + yes + + + Extended Next Hop Encoding(5) + 000100010002000100800002 + + + Graceful Restart(64) + 007800010100 + + + 4-Byte AS Number(65) + 64711 + + + Deprecated(66) + yes + + + Dynamic Capability(67) + 020140 + + + Route Refresh (Cisco)(128) + yes + + + + + 12 + 12 + 0 + 0 + 78 + 78 + + + + + POC + 1.1.1.1 + 64712 + Established + 17840219 + no + no + 1 + 1.1.1.1:179 + 1.1.1.1:49498 + not-client + no + yes + Unspecified + 15 + 0 + 15 + 0 + 90 + 90 + 30 + 30 + 7224 + 10237 + 314761 + 361928 + 26 + + 4 + 1 + 0 + no + yes + no + + no + + + + Multiprotocol Extensions(1) + IPv4 Unicast + + + Route Refresh(2) + yes + + + Extended Next Hop Encoding(5) + 000100010002000100800002 + + + Graceful Restart(64) + 007800010100 + + + 4-Byte AS Number(65) + 64712 + + + Deprecated(66) + yes + + + Dynamic Capability(67) + 020140 + + + Route Refresh (Cisco)(128) + yes + + + + + 12 + 12 + 0 + 0 + 84 + 84 + + + + + POC + 1.1.1.1 + 64712 + Established + 17840218 + no + no + 1 + 1.1.1.1:179 + 1.1.1.1:55026 + not-client + no + yes + Unspecified + 15 + 0 + 15 + 0 + 90 + 90 + 30 + 30 + 7183 + 10236 + 314725 + 362057 + 26 + + 4 + 1 + 0 + no + yes + no + + no + + + + Multiprotocol Extensions(1) + IPv4 Unicast + + + Route Refresh(2) + yes + + + Extended Next Hop Encoding(5) + 000100010002000100800002 + + + Graceful Restart(64) + 007800010100 + + + 4-Byte AS Number(65) + 64712 + + + Deprecated(66) + yes + + + Dynamic Capability(67) + 020140 + + + Route Refresh (Cisco)(128) + yes + + + + + 12 + 12 + 0 + 0 + 84 + 84 + + + + + + \ No newline at end of file diff --git a/x-pack/metricbeat/module/panw/client.go b/x-pack/metricbeat/module/panw/client.go new file mode 100644 index 00000000000..4b670a89dce --- /dev/null +++ b/x-pack/metricbeat/module/panw/client.go @@ -0,0 +1,50 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package panw + +import ( + "flag" + "fmt" + + "github.com/PaloAltoNetworks/pango" +) + +// Vsys is the virtual system to query. If empty, the default vsys is used. This is a placeholder for future use, +// as the module currently only supports the default vsys. +const Vsys = "" + +// PanwClient interface with an Op function +type PanwClient interface { + Op(req interface{}, vsys string, extras, ans interface{}) ([]byte, error) +} + +type PanwFirewallClient struct { + pango.Firewall +} + +type PanwTestClient struct { +} + +// Op is a mock function for testing that returns sample XML output based on the initial req parameter +// XML output is stored in the testdata directory, one file per query string +func (c *PanwTestClient) Op(req interface{}, vsys string, extras, ans interface{}) ([]byte, error) { + return nil, nil +} + +func GetPanwClient(config *Config) (PanwClient, error) { + // If running tests, return a test client + if flag.Lookup("test.v") != nil { + return &PanwTestClient{}, nil + } + + firewall := pango.Firewall{Client: pango.Client{Hostname: config.HostIp, ApiKey: config.ApiKey, Port: config.Port}} + err := firewall.Initialize() + if err != nil { + return nil, fmt.Errorf("error initializing firewall client: %w", err) + } + // Instantiate panwFirewallClient + return &PanwFirewallClient{Firewall: firewall}, nil + +} diff --git a/x-pack/metricbeat/module/panw/config.go b/x-pack/metricbeat/module/panw/config.go new file mode 100644 index 00000000000..30a14bd12c6 --- /dev/null +++ b/x-pack/metricbeat/module/panw/config.go @@ -0,0 +1,30 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package panw + +import ( + "github.com/elastic/beats/v7/metricbeat/mb" +) + +const ( + ModuleName = "panw" +) + +type Config struct { + HostIp string `config:"host_ip" validate:"required"` + ApiKey string `config:"api_key" validate:"required"` + Port uint `config:"port"` + DebugMode string `config:"api_debug_mode"` +} + +func NewConfig(base mb.BaseMetricSet) (*Config, error) { + config := Config{} + if err := base.Module().UnpackConfig(&config); err != nil { + return nil, err + } + + return &config, nil + +} diff --git a/x-pack/metricbeat/module/panw/doc.go b/x-pack/metricbeat/module/panw/doc.go new file mode 100644 index 00000000000..21f5ce0404f --- /dev/null +++ b/x-pack/metricbeat/module/panw/doc.go @@ -0,0 +1,6 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +// Package panw is a Metricbeat module that contains MetricSets. +package panw diff --git a/x-pack/metricbeat/module/panw/fields.go b/x-pack/metricbeat/module/panw/fields.go new file mode 100644 index 00000000000..c63f4af7486 --- /dev/null +++ b/x-pack/metricbeat/module/panw/fields.go @@ -0,0 +1,23 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +// Code generated by beats/dev-tools/cmd/asset/asset.go - DO NOT EDIT. + +package panw + +import ( + "github.com/elastic/beats/v7/libbeat/asset" +) + +func init() { + if err := asset.SetFields("metricbeat", "panw", asset.ModuleFieldsPri, AssetPanw); err != nil { + panic(err) + } +} + +// AssetPanw returns asset data. +// This is the base64 encoded zlib format compressed contents of module/panw. +func AssetPanw() string { + return "eJzUz0FOBCEQheE9p3iZfV+AnRfQSTwBDm8mZKAgUNhye9NGO21rXNtvSSVffibcOSyKk9kAGjTS4nR2Mp8M4NkuNRQNWSzOD4/T0zNS9j3SAJWRrtHiheoMcA2MvlkDABPEJa7uMh2FFreae/l8+VNfthW3ahBlvboL23r6zV+2j/zant7yfHOprBHf/TvHnKvf3XxIlPbxD62dP4Jr7hrkdpDaNpoyHST2tcg/LX0PAAD//4Q63bA=" +} diff --git a/x-pack/metricbeat/module/panw/interfaces/_meta/data.json b/x-pack/metricbeat/module/panw/interfaces/_meta/data.json new file mode 100644 index 00000000000..50315d045c7 --- /dev/null +++ b/x-pack/metricbeat/module/panw/interfaces/_meta/data.json @@ -0,0 +1,19 @@ +{ + "@timestamp":"2016-05-23T08:05:34.853Z", + "beat":{ + "hostname":"beathost", + "name":"beathost" + }, + "metricset":{ + "host":"localhost", + "module":"panw", + "name":"interfaces", + "rtt":44269 + }, + "panw":{ + "interfaces":{ + "example": "interfaces" + } + }, + "type":"metricsets" +} diff --git a/x-pack/metricbeat/module/panw/interfaces/_meta/docs.asciidoc b/x-pack/metricbeat/module/panw/interfaces/_meta/docs.asciidoc new file mode 100644 index 00000000000..affd907c295 --- /dev/null +++ b/x-pack/metricbeat/module/panw/interfaces/_meta/docs.asciidoc @@ -0,0 +1 @@ +This is the interfaces metricset of the module panw. diff --git a/x-pack/metricbeat/module/panw/interfaces/_meta/fields.yml b/x-pack/metricbeat/module/panw/interfaces/_meta/fields.yml new file mode 100644 index 00000000000..7e7dacf152c --- /dev/null +++ b/x-pack/metricbeat/module/panw/interfaces/_meta/fields.yml @@ -0,0 +1,7 @@ +- name: interfaces + type: group + release: beta + fields: + - name: example + type: keyword + dimension: true \ No newline at end of file diff --git a/x-pack/metricbeat/module/panw/interfaces/ha_interfaces.go b/x-pack/metricbeat/module/panw/interfaces/ha_interfaces.go new file mode 100644 index 00000000000..7228eca2955 --- /dev/null +++ b/x-pack/metricbeat/module/panw/interfaces/ha_interfaces.go @@ -0,0 +1,183 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package interfaces + +import ( + "encoding/xml" + "time" + + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + "github.com/elastic/elastic-agent-libs/logp" + "github.com/elastic/elastic-agent-libs/mapstr" +) + +const haInterfaceQuery = "" + +var haLogger *logp.Logger + +func getHAInterfaceEvents(m *MetricSet) ([]mb.Event, error) { + // Set logger so all the parse functions have access + haLogger = m.logger + var response HAResponse + + output, err := m.client.Op(haInterfaceQuery, panw.Vsys, nil, nil) + if err != nil { + haLogger.Error("Error: %s", err) + return nil, err + } + + err = xml.Unmarshal(output, &response) + if err != nil { + haLogger.Errorw("Failed to unmarshal interface response", "error", err, "query", haInterfaceQuery) + return nil, err + } + + events := formatHAInterfaceEvents(m, response.Result) + + return events, nil + +} + +func formatHAInterfaceEvents(m *MetricSet, input HAResult) []mb.Event { + events := make([]mb.Event, 0, len(input.Group.LinkMonitoring.Groups)+1) + group := input.Group + + groupEvent := makeGroupEvent(m, input) + events = append(events, *groupEvent) + linkMonitorEvents := makeLinkMonitoringEvents(m, group.LinkMonitoring) + events = append(events, linkMonitorEvents...) + + return events +} + +func makeGroupEvent(m *MetricSet, input HAResult) *mb.Event { + + group := input.Group + timestamp := time.Now().UTC() + rootFields := panw.MakeRootFields(m.config.HostIp) + + linkMonitoringEnabled, err := panw.StringToBool(group.LinkMonitoring.Enabled) + if err != nil { + haLogger.Warn("Error converting LinkMonitoring.Enabled to boolean: %s", err) + } + enabled, err := panw.StringToBool(input.Enabled) + if err != nil { + haLogger.Warn("Error converting Enabled to boolean: %s", err) + } + syncEnabled, err := panw.StringToBool(group.RunningSyncEnabled) + if err != nil { + haLogger.Warn("Error converting RunningSyncEnabled to boolean: %s", err) + } + + event := mb.Event{ + Timestamp: timestamp, + MetricSetFields: mapstr.M{ + "ha.enabled": enabled, + "ha.mode": group.Mode, + "ha.running_sync": group.RunningSync, + "ha.running_sync_enabled": syncEnabled, + "ha.local_info.version": group.LocalInfo.Version, + "ha.local_info.state": group.LocalInfo.State, + "ha.local_info.state_duration": group.LocalInfo.StateDuration, + "ha.local_info.mgmt_ip": group.LocalInfo.MgmtIP, + "ha.local_info.preemptive": group.LocalInfo.Preemptive, + "ha.local_info.mode": group.LocalInfo.Mode, + "ha.local_info.platform_model": group.LocalInfo.PlatformModel, + "ha.local_info.state_sync": group.LocalInfo.StateSync, + "ha.local_info.state_sync_type": group.LocalInfo.StateSyncType, + "ha.local_info.ha1_ipaddr": group.LocalInfo.HA1IPAddr, + "ha.local_info.ha1_macaddr": group.LocalInfo.HA1MACAddr, + "ha.local_info.ha1_port": group.LocalInfo.HA1Port, + "ha.local_info.ha1_backup_ipaddr": group.LocalInfo.HA1BackupIPAddr, + "ha.local_info.ha1_backup_macaddr": group.LocalInfo.HA1BackupMACAddr, + "ha.local_info.ha1_backup_port": group.LocalInfo.HA1BackupPort, + "ha.local_info.ha1_backup_gateway": group.LocalInfo.HA1BackupGateway, + "ha.local_info.ha2_ipaddr": group.LocalInfo.HA2IPAddr, + "ha.local_info.ha2_macaddr": group.LocalInfo.HA2MACAddr, + "ha.local_info.ha2_port": group.LocalInfo.HA2Port, + "ha.local_info.build_rel": group.LocalInfo.BuildRel, + "ha.local_info.url_version": group.LocalInfo.URLVersion, + "ha.local_info.app_version": group.LocalInfo.AppVersion, + "ha.local_info.iot_version": group.LocalInfo.IoTVersion, + "ha.local_info.av_version": group.LocalInfo.AVVersion, + "ha.local_info.threat_version": group.LocalInfo.ThreatVersion, + "ha.local_info.vpn_client_version": group.LocalInfo.VPNClientVersion, + "ha.local_info.gp_client_version": group.LocalInfo.GPClientVersion, + "ha.peer_info.conn_status": group.PeerInfo.ConnStatus, + "ha.peer_info.state": group.PeerInfo.State, + "ha.peer_info.state_duration": group.PeerInfo.StateDuration, + "ha.peer_info.mgmt_ip": group.PeerInfo.MgmtIP, + "ha.peer_info.preemptive": group.PeerInfo.Preemptive, + "ha.peer_info.mode": group.PeerInfo.Mode, + "ha.peer_info.platform_model": group.PeerInfo.PlatformModel, + "ha.peer_info.priority": group.PeerInfo.Priority, + "ha.peer_info.ha1_ipaddr": group.PeerInfo.HA1IPAddr, + "ha.peer_info.ha1_macaddr": group.PeerInfo.HA1MACAddr, + "ha.peer_info.ha1_backup_ipaddr": group.PeerInfo.HA1BackupIPAddr, + "ha.peer_info.ha1_backup_macaddr": group.PeerInfo.HA1BackupMACAddr, + "ha.peer_info.ha2_ipaddr": group.PeerInfo.HA2IPAddr, + "ha.peer_info.ha2_macaddr": group.PeerInfo.HA2MACAddr, + "ha.peer_info.conn_ha1.status": group.PeerInfo.ConnHA1.Status, + "ha.peer_info.conn_ha1.primary": group.PeerInfo.ConnHA1.Primary, + "ha.peer_info.conn_ha1.description": group.PeerInfo.ConnHA1.Desc, + "ha.peer_info.conn_ha2.status": group.PeerInfo.ConnHA2.Status, + "ha.peer_info.conn_ha2.primary": group.PeerInfo.ConnHA2.Primary, + "ha.peer_info.conn_ha2.description": group.PeerInfo.ConnHA2.Desc, + "ha.peer_info.conn_ha1_backup.status": group.PeerInfo.ConnHA1Backup.Status, + "ha.peer_info.conn_ha1_backup.description": group.PeerInfo.ConnHA1Backup.Desc, + "ha.link_monitoring.enabled": linkMonitoringEnabled, + }, + RootFields: rootFields, + } + + return &event +} + +func makeLinkMonitoringEvents(m *MetricSet, links HALinkMonitoring) []mb.Event { + if len(links.Groups) == 0 { + return nil + } + + events := make([]mb.Event, 0, len(links.Groups)) + timestamp := time.Now().UTC() + rootFields := panw.MakeRootFields(m.config.HostIp) + + var event mb.Event + for _, group := range links.Groups { + if group.Interface == nil { + haLogger.Warn("No interface entries found in link monitoring group: %s", group.Name) + continue + } + for _, interface_entry := range group.Interface { + linkEnabled, err := panw.StringToBool(links.Enabled) + if err != nil { + haLogger.Warn("Error converting links.Enabled to boolean: %s", err) + } + groupEnabled, err := panw.StringToBool(group.Enabled) + if err != nil { + haLogger.Warn("Error converting group.Enabled to boolean: %s", err) + } + + event = mb.Event{ + Timestamp: timestamp, + MetricSetFields: mapstr.M{ + "ha.link_monitoring.enabled": linkEnabled, + "ha.link_monitoring.failure_condition": links.FailureCondition, + "ha.link_monitoring.group.name": group.Name, + "ha.link_monitoring.group.enabled": groupEnabled, + "ha.link_monitoring.group.failure_condition": group.FailureCondition, + "ha.link_monitoring.group.interface.name": interface_entry.Name, + "ha.link_monitoring.group.interface.status": interface_entry.Status, + }, + RootFields: rootFields, + } + } + + events = append(events, event) + } + + return events +} diff --git a/x-pack/metricbeat/module/panw/interfaces/ifnet_interfaces.go b/x-pack/metricbeat/module/panw/interfaces/ifnet_interfaces.go new file mode 100644 index 00000000000..a94d1522b31 --- /dev/null +++ b/x-pack/metricbeat/module/panw/interfaces/ifnet_interfaces.go @@ -0,0 +1,115 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package interfaces + +import ( + "encoding/xml" + "time" + + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + "github.com/elastic/elastic-agent-libs/mapstr" +) + +// these types apply to phyiscal interfaces +var interfaceTypes = map[int]string{ + 0: "Ethernet interface", + 1: "Aggregate Ethernet (AE) interface", + 2: "High Availability (HA) interface", + 3: "VLAN interface", + 5: "Loopback interface", + 6: "Tunnel interface", + 10: "SD-WAN interface", +} + +const IFNetInterfaceQuery = "all" + +func getIFNetInterfaceEvents(m *MetricSet) ([]mb.Event, error) { + + var response InterfaceResponse + + output, err := m.client.Op(IFNetInterfaceQuery, panw.Vsys, nil, nil) + if err != nil { + m.logger.Error("Error: %s", err) + return nil, err + } + + err = xml.Unmarshal(output, &response) + if err != nil { + m.logger.Error("Error: %s", err) + return nil, err + } + + events := formatIFInterfaceEvents(m, response.Result) + + return events, nil + +} + +func formatIFInterfaceEvents(m *MetricSet, input InterfaceResult) []mb.Event { + events := make([]mb.Event, 0, len(input.HW.Entries)+len(input.Ifnet.Entries)) + timestamp := time.Now().UTC() + rootFields := panw.MakeRootFields(m.config.HostIp) + + // First process the phyiscal interfaces + for _, entry := range input.HW.Entries { + iftype, ok := interfaceTypes[entry.Type] + + if !ok { + m.logger.Warnw("Unknown interface type", "type", entry.Type) + iftype = "Unknown" + } + + var members []string + // If this is an aggregate interface, populate the members + if entry.Type == 1 && len(entry.AEMember.Members) != 0 { + members = entry.AEMember.Members + } + + event := mb.Event{ + Timestamp: timestamp, + MetricSetFields: mapstr.M{ + "physical.name": entry.Name, + "physical.id": entry.ID, + "physical.type": iftype, + "physical.mac": entry.MAC, + "physical.speed": entry.Speed, + "physical.duplex": entry.Duplex, + "physical.state": entry.State, + "physical.mode": entry.Mode, + "physical.full_state": entry.ST, + "physical.ae_member": members, + }, + RootFields: rootFields, + } + + events = append(events, event) + } + + // Now process the logical interfaces + for _, entry := range input.Ifnet.Entries { + event := mb.Event{ + Timestamp: timestamp, + MetricSetFields: mapstr.M{ + "logical.name": entry.Name, + "logical.id": entry.ID, + "logical.tag": entry.Tag, + "logical.vsys": entry.Vsys, + "logical.zone": entry.Zone, + "logical.fwd": entry.Fwd, + "logical.ip": entry.IP, + "logical.addr": entry.Addr, + "logical.dyn_addr": entry.DynAddr, + "logical.addr6": entry.Addr6, + }, + RootFields: rootFields, + } + + events = append(events, event) + + } + + return events +} diff --git a/x-pack/metricbeat/module/panw/interfaces/interface_types.go b/x-pack/metricbeat/module/panw/interfaces/interface_types.go new file mode 100644 index 00000000000..b1f3e944562 --- /dev/null +++ b/x-pack/metricbeat/module/panw/interfaces/interface_types.go @@ -0,0 +1,249 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package interfaces + +import "encoding/xml" + +type InterfaceResponse struct { + XMLName xml.Name `xml:"response"` + Status string `xml:"status,attr"` + Result InterfaceResult `xml:"result"` +} + +type InterfaceResult struct { + HW HW `xml:"hw"` + Ifnet Ifnet `xml:"ifnet"` +} + +type HW struct { + Entries []HWInterfaceEntry `xml:"entry"` +} + +type HWInterfaceEntry struct { + Name string `xml:"name"` + ID int `xml:"id"` + Type int `xml:"type"` + MAC string `xml:"mac"` + Speed string `xml:"speed"` + Duplex string `xml:"duplex"` + State string `xml:"state"` + Mode string `xml:"mode"` + ST string `xml:"st"` + AEMember AEMember `xml:"ae_member"` +} + +type IFInterfaceEntry struct { + Name string `xml:"name"` + ID int `xml:"id"` + Tag int `xml:"tag"` + Vsys int `xml:"vsys"` + Zone string `xml:"zone"` + Fwd string `xml:"fwd"` + IP string `xml:"ip"` + Addr string `xml:"addr"` + DynAddr string `xml:"dyn-addr"` + Addr6 string `xml:"addr6"` +} + +type AEMember struct { + Members []string `xml:"member"` +} + +type Ifnet struct { + Entries []IFInterfaceEntry `xml:"entry"` +} + +// HA Interfaces + +type HAResponse struct { + XMLName xml.Name `xml:"response"` + Status string `xml:"status,attr"` + Result HAResult `xml:"result"` +} + +type HAResult struct { + Enabled string `xml:"enabled"` + Group HAGroup `xml:"group"` +} + +type HAGroup struct { + Mode string `xml:"mode"` + LocalInfo HALocalInfo `xml:"local-info"` + PeerInfo HAPeerInfo `xml:"peer-info"` + LinkMonitoring HALinkMonitoring `xml:"link-monitoring"` + PathMonitoring HAPathMonitoring `xml:"path-monitoring"` + RunningSync string `xml:"running-sync"` + RunningSyncEnabled string `xml:"running-sync-enabled"` +} + +type HALocalInfo struct { + Version string `xml:"version"` + State string `xml:"state"` + StateDuration int `xml:"state-duration"` + MgmtIP string `xml:"mgmt-ip"` + MgmtIPv6 string `xml:"mgmt-ipv6"` + Preemptive string `xml:"preemptive"` + PromotionHold int `xml:"promotion-hold"` + HelloInterval int `xml:"hello-interval"` + HeartbeatInterval int `xml:"heartbeat-interval"` + PreemptHold int `xml:"preempt-hold"` + MonitorFailHoldup int `xml:"monitor-fail-holdup"` + AddonMasterHoldup int `xml:"addon-master-holdup"` + HA1EncryptImported string `xml:"ha1-encrypt-imported"` + Mode string `xml:"mode"` + PlatformModel string `xml:"platform-model"` + Priority int `xml:"priority"` + MaxFlaps int `xml:"max-flaps"` + PreemptFlapCnt int `xml:"preempt-flap-cnt"` + NonfuncFlapCnt int `xml:"nonfunc-flap-cnt"` + StateSync string `xml:"state-sync"` + StateSyncType string `xml:"state-sync-type"` + ActivePassive ActivePassive `xml:"active-passive"` + HA1IPAddr string `xml:"ha1-ipaddr"` + HA1MACAddr string `xml:"ha1-macaddr"` + HA1Port string `xml:"ha1-port"` + HA1EncryptEnable string `xml:"ha1-encrypt-enable"` + HA1LinkMonIntv int `xml:"ha1-link-mon-intv"` + HA1BackupIPAddr string `xml:"ha1-backup-ipaddr"` + HA1BackupMACAddr string `xml:"ha1-backup-macaddr"` + HA1BackupPort string `xml:"ha1-backup-port"` + HA1BackupGateway string `xml:"ha1-backup-gateway"` + HA2IPAddr string `xml:"ha2-ipaddr"` + HA2MACAddr string `xml:"ha2-macaddr"` + HA2Port string `xml:"ha2-port"` + BuildRel string `xml:"build-rel"` + URLVersion string `xml:"url-version"` + AppVersion string `xml:"app-version"` + IoTVersion string `xml:"iot-version"` + AVVersion string `xml:"av-version"` + ThreatVersion string `xml:"threat-version"` + VPNClientVersion string `xml:"vpnclient-version"` + GPClientVersion string `xml:"gpclient-version"` + DLP string `xml:"DLP"` + BuildCompat string `xml:"build-compat"` + URLCompat string `xml:"url-compat"` + AppCompat string `xml:"app-compat"` + IoTCompat string `xml:"iot-compat"` + AVCompat string `xml:"av-compat"` + ThreatCompat string `xml:"threat-compat"` + VPNClientCompat string `xml:"vpnclient-compat"` + GPClientCompat string `xml:"gpclient-compat"` +} + +type ActivePassive struct { + PassiveLinkState string `xml:"passive-link-state"` + MonitorFailHolddown int `xml:"monitor-fail-holddown"` +} + +type HAPeerInfo struct { + ConnHA1 ConnHA1 `xml:"conn-ha1"` + ConnHA1Backup ConnHA1Backup `xml:"conn-ha1-backup"` + ConnHA2 ConnHA2 `xml:"conn-ha2"` + ConnStatus string `xml:"conn-status"` + Version string `xml:"version"` + State string `xml:"state"` + StateDuration int `xml:"state-duration"` + LastErrorReason string `xml:"last-error-reason"` + LastErrorState string `xml:"last-error-state"` + Preemptive string `xml:"preemptive"` + Mode string `xml:"mode"` + PlatformModel string `xml:"platform-model"` + VMLicense string `xml:"vm-license"` + Priority int `xml:"priority"` + MgmtIP string `xml:"mgmt-ip"` + MgmtIPv6 string `xml:"mgmt-ipv6"` + HA1IPAddr string `xml:"ha1-ipaddr"` + HA1MACAddr string `xml:"ha1-macaddr"` + HA1BackupIPAddr string `xml:"ha1-backup-ipaddr"` + HA1BackupMACAddr string `xml:"ha1-backup-macaddr"` + HA2IPAddr string `xml:"ha2-ipaddr"` + HA2MACAddr string `xml:"ha2-macaddr"` + BuildRel string `xml:"build-rel"` + URLVersion string `xml:"url-version"` + AppVersion string `xml:"app-version"` + IoTVersion string `xml:"iot-version"` + AVVersion string `xml:"av-version"` + ThreatVersion string `xml:"threat-version"` + VPNClientVersion string `xml:"vpnclient-version"` + GPClientVersion string `xml:"gpclient-version"` + DLP string `xml:"DLP"` +} + +type ConnHA1 struct { + Status string `xml:"conn-status"` + Primary string `xml:"conn-primary"` + Desc string `xml:"conn-desc"` +} + +type ConnHA1Backup struct { + Status string `xml:"conn-status"` + Desc string `xml:"conn-desc"` +} + +type ConnHA2 struct { + Primary string `xml:"conn-primary"` + KAEnabled string `xml:"conn-ka-enbled"` + Desc string `xml:"conn-desc"` + Status string `xml:"conn-status"` +} + +type HALinkMonitoring struct { + Enabled string `xml:"enabled"` + FailureCondition string `xml:"failure-condition"` + Groups []HAGroupEntry `xml:"groups>entry"` +} + +type HAGroupEntry struct { + Name string `xml:"name"` + Enabled string `xml:"enabled"` + FailureCondition string `xml:"failure-condition"` + Interface []HAInterfaceEntry `xml:"interface>entry"` +} + +type HAInterfaceEntry struct { + Name string `xml:"name"` + Status string `xml:"status"` +} + +type HAPathMonitoring struct { + Enabled string `xml:"enabled"` + FailureCondition string `xml:"failure-condition"` + VirtualWire string `xml:"virtual-wire"` + VLAN string `xml:"vlan"` + VirtualRouter string `xml:"virtual-router"` +} + +// IPSec tunnels + +type TunnelsResponse struct { + Status string `xml:"status,attr"` + Result TunnelsResult `xml:"result"` +} + +type TunnelsResult struct { + Entries []TunnelsEntry `xml:"entries>entry"` + NTun int `xml:"ntun"` +} + +type TunnelsEntry struct { + ID int `xml:"id"` + Name string `xml:"name"` + GW string `xml:"gw"` + TSiIP string `xml:"TSi_ip"` + TSiPrefix int `xml:"TSi_prefix"` + TSiProto int `xml:"TSi_proto"` + TSiPort int `xml:"TSi_port"` + TSrIP string `xml:"TSr_ip"` + TSrPrefix int `xml:"TSr_prefix"` + TSrProto int `xml:"TSr_proto"` + TSrPort int `xml:"TSr_port"` + Proto string `xml:"proto"` + Mode string `xml:"mode"` + DH string `xml:"dh"` + Enc string `xml:"enc"` + Hash string `xml:"hash"` + Life int `xml:"life"` + KB int `xml:"kb"` +} diff --git a/x-pack/metricbeat/module/panw/interfaces/interfaces.go b/x-pack/metricbeat/module/panw/interfaces/interfaces.go new file mode 100644 index 00000000000..2f93798f53d --- /dev/null +++ b/x-pack/metricbeat/module/panw/interfaces/interfaces.go @@ -0,0 +1,101 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package interfaces + +import ( + "errors" + "fmt" + + "github.com/elastic/beats/v7/libbeat/common/cfgwarn" + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + "github.com/elastic/elastic-agent-libs/logp" +) + +const ( + metricsetName = "interfaces" +) + +// MetricSet holds any configuration or state information. It must implement +// the mb.MetricSet interface. And this is best achieved by embedding +// mb.BaseMetricSet because it implements all of the required mb.MetricSet +// interface methods except for Fetch. +type MetricSet struct { + mb.BaseMetricSet + config *panw.Config + logger *logp.Logger + client panw.PanwClient +} + +// init registers the MetricSet with the central registry as soon as the program +// starts. The New function will be called later to instantiate an instance of +// the MetricSet for each host is defined in the module's configuration. After the +// MetricSet has been created then Fetch will begin to be called periodically. +func init() { + mb.Registry.MustAddMetricSet(panw.ModuleName, metricsetName, New) +} + +// New creates a new instance of the MetricSet. New is responsible for unpacking +// any MetricSet specific configuration options if there are any. +func New(base mb.BaseMetricSet) (mb.MetricSet, error) { + cfgwarn.Beta("The panw interfaces metricset is beta.") + + config, err := panw.NewConfig(base) + if err != nil { + return nil, err + } + + logger := logp.NewLogger(base.FullyQualifiedName()) + + //client := &pango.Firewall{Client: pango.Client{Hostname: config.HostIp, ApiKey: config.ApiKey}} + client, err := panw.GetPanwClient(config) + if err != nil { + return nil, err + } + + return &MetricSet{ + BaseMetricSet: base, + config: config, + logger: logger, + client: client, + }, nil +} + +// Fetch method implements the data gathering and data conversion to the right +// format. It publishes the event which is then forwarded to the output. In case +// of an error set the Error field of mb.Event or simply call report.Error(). +func (m *MetricSet) Fetch(report mb.ReporterV2) error { + // accumulate errs and report them all at the end so that we don't + // stop processing events if one of the fetches fails + var errs []error + + eventFetchers := []struct { + name string + fn func(*MetricSet) ([]mb.Event, error) + }{ + {"ifnet interfaces", getIFNetInterfaceEvents}, + {"HA interfaces", getHAInterfaceEvents}, + {"ipsec tunnel", getIPSecTunnelEvents}, + } + + for _, fetcher := range eventFetchers { + events, err := fetcher.fn(m) + if err != nil { + m.logger.Errorf("Error getting %s events: %s", fetcher.name, err) + errs = append(errs, err) + } else { + for _, event := range events { + report.Event(event) + } + } + } + + if len(errs) > 0 { + return fmt.Errorf("error while fetching vpn metrics: %w", errors.Join(errs...)) + } + + return nil + +} diff --git a/x-pack/metricbeat/module/panw/interfaces/tunnels.go b/x-pack/metricbeat/module/panw/interfaces/tunnels.go new file mode 100644 index 00000000000..df01bec93dd --- /dev/null +++ b/x-pack/metricbeat/module/panw/interfaces/tunnels.go @@ -0,0 +1,81 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package interfaces + +import ( + "encoding/xml" + "fmt" + "time" + + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + "github.com/elastic/elastic-agent-libs/mapstr" +) + +const IPSecTunnelsQuery = "" + +func getIPSecTunnelEvents(m *MetricSet) ([]mb.Event, error) { + + var response TunnelsResponse + + output, err := m.client.Op(IPSecTunnelsQuery, panw.Vsys, nil, nil) + if err != nil { + m.logger.Error("Error: %s", err) + return nil, fmt.Errorf("error querying IPSec tunnels: %w", err) + } + + err = xml.Unmarshal(output, &response) + if err != nil { + m.logger.Error("Error: %s", err) + return nil, fmt.Errorf("error unmarshaling IPSec tunnels response: %w", err) + } + + events := formatIPSecTunnelEvents(m, response.Result.Entries) + + return events, nil + +} + +func formatIPSecTunnelEvents(m *MetricSet, entries []TunnelsEntry) []mb.Event { + if entries == nil { + return nil + } + + events := make([]mb.Event, 0, len(entries)) + timestamp := time.Now().UTC() + rootFields := panw.MakeRootFields(m.config.HostIp) + + for _, entry := range entries { + event := mb.Event{ + Timestamp: timestamp, + MetricSetFields: mapstr.M{ + "ipsec_tunnel.id": entry.ID, + "ipsec_tunnel.name": entry.Name, + "ipsec_tunnel.gw": entry.GW, + "ipsec_tunnel.TSi_ip": entry.TSiIP, + "ipsec_tunnel.TSi_prefix": entry.TSiPrefix, + "ipsec_tunnel.TSi_proto": entry.TSiProto, + "ipsec_tunnel.TSi_port": entry.TSiPort, + "ipsec_tunnel.TSr_ip": entry.TSrIP, + "ipsec_tunnel.TSr_prefix": entry.TSrPrefix, + "ipsec_tunnel.TSr_proto": entry.TSrProto, + "ipsec_tunnel.TSr_port": entry.TSrPort, + "ipsec_tunnel.proto": entry.Proto, + "ipsec_tunnel.mode": entry.Mode, + "ipsec_tunnel.dh": entry.DH, + "ipsec_tunnel.enc": entry.Enc, + "ipsec_tunnel.hash": entry.Hash, + "ipsec_tunnel.life.sec": entry.Life, + "ipsec_tunnel.kb": entry.KB, + }, + RootFields: rootFields, + } + + events = append(events, event) + } + + return events + +} diff --git a/x-pack/metricbeat/module/panw/routing/_meta/data.json b/x-pack/metricbeat/module/panw/routing/_meta/data.json new file mode 100644 index 00000000000..133eaa06d18 --- /dev/null +++ b/x-pack/metricbeat/module/panw/routing/_meta/data.json @@ -0,0 +1,19 @@ +{ + "@timestamp":"2016-05-23T08:05:34.853Z", + "beat":{ + "hostname":"beathost", + "name":"beathost" + }, + "metricset":{ + "host":"localhost", + "module":"panw", + "name":"routing", + "rtt":44269 + }, + "panw":{ + "routing":{ + "example": "routing" + } + }, + "type":"metricsets" +} diff --git a/x-pack/metricbeat/module/panw/routing/_meta/docs.asciidoc b/x-pack/metricbeat/module/panw/routing/_meta/docs.asciidoc new file mode 100644 index 00000000000..a0007378f3a --- /dev/null +++ b/x-pack/metricbeat/module/panw/routing/_meta/docs.asciidoc @@ -0,0 +1 @@ +This is the routing metricset of the module panw. diff --git a/x-pack/metricbeat/module/panw/routing/_meta/fields.yml b/x-pack/metricbeat/module/panw/routing/_meta/fields.yml new file mode 100644 index 00000000000..98c9cba48a3 --- /dev/null +++ b/x-pack/metricbeat/module/panw/routing/_meta/fields.yml @@ -0,0 +1,7 @@ +- name: routing + type: group + release: beta + fields: + - name: example + type: keyword + dimension: true \ No newline at end of file diff --git a/x-pack/metricbeat/module/panw/routing/bgp_peers.go b/x-pack/metricbeat/module/panw/routing/bgp_peers.go new file mode 100644 index 00000000000..a99ec08c6d7 --- /dev/null +++ b/x-pack/metricbeat/module/panw/routing/bgp_peers.go @@ -0,0 +1,140 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package routing + +import ( + "encoding/xml" + "net" + "time" + + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + "github.com/elastic/elastic-agent-libs/logp" + "github.com/elastic/elastic-agent-libs/mapstr" +) + +const bgpPeersQuery = "" + +var bgpLogger *logp.Logger + +func getBGPEvents(m *MetricSet) ([]mb.Event, error) { + // Set logger so all the sub functions have access + bgpLogger = m.logger + var response BGPResponse + + output, err := m.client.Op(bgpPeersQuery, panw.Vsys, nil, nil) + if err != nil { + m.logger.Error("Error calling API: %s", err) + return nil, err + } + + err = xml.Unmarshal(output, &response) + if err != nil { + m.logger.Error("Error unmarshalling: %s", err) + return nil, err + } + + events := formatBGPEvents(m, response.Result.Entries) + + return events, nil +} + +// Convert yes/no strings to booleans. Convert any errors to false and log a warning +func convertEntryBooleanFields(entry BGPEntry) map[string]bool { + + fields := []struct { + name string + value string + }{ + {"bgp.password_set", entry.PasswordSet}, + {"bgp.passive", entry.Passive}, + {"bgp.same_confederation", entry.SameConfederation}, + {"bgp.aggregate_confed_as", entry.AggregateConfedAS}, + {"bgp.nexthop_self", entry.NexthopSelf}, + {"bgp.nexthop_thirdparty", entry.NexthopThirdparty}, + {"bgp.nexthop_peer", entry.NexthopPeer}, + } + + result := make(map[string]bool) + for _, field := range fields { + boolValue, err := panw.StringToBool(field.value) + if err != nil { + bgpLogger.Warnf("Error converting %s: %v", field.name, err) + boolValue = false + } + result[field.name] = boolValue + } + + return result +} + +func formatBGPEvents(m *MetricSet, entries []BGPEntry) []mb.Event { + events := make([]mb.Event, 0, len(entries)) + timestamp := time.Now().UTC() + rootFields := panw.MakeRootFields(m.config.HostIp) + + for _, entry := range entries { + booleanFields := convertEntryBooleanFields(entry) + peer_ip, peer_port, err := net.SplitHostPort(entry.PeerAddress) + if err != nil { + bgpLogger.Warnf("Error splitting peer address (%s): %v", entry.PeerAddress, err) + peer_ip = entry.PeerAddress + } + local_ip, local_port, err := net.SplitHostPort(entry.LocalAddress) + if err != nil { + bgpLogger.Warnf("Error splitting local address (%s): %v", entry.LocalAddress, err) + local_ip = entry.LocalAddress + } + + event := mb.Event{ + Timestamp: timestamp, + MetricSetFields: mapstr.M{ + "bgp.peer_name": entry.Peer, + "bgp.virtual_router": entry.Vr, + "bgp.peer_group": entry.PeerGroup, + "bgp.peer_router_id": entry.PeerRouterID, + "bgp.remote_as_asn": entry.RemoteAS, + "bgp.status": entry.Status, + "bgp.status_duration": entry.StatusDuration, + "bgp.password_set": booleanFields["bgp.password_set"], + "bgp.passive": booleanFields["bgp.passive"], + "bgp.multi_hop_ttl": entry.MultiHopTTL, + "bgp.peer_ip": peer_ip, + "bgp.peer_port": peer_port, + "bgp.local_ip": local_ip, + "bgp.local_port": local_port, + "bgp.reflector_client": entry.ReflectorClient, + "bgp.same_confederation": booleanFields["bgp.same_confederation"], + "bgp.aggregate_confed_as": booleanFields["bgp.aggregate_confed_as"], + "bgp.peering_type": entry.PeeringType, + "bgp.connect_retry_interval": entry.ConnectRetryInterval, + "bgp.open_delay": entry.OpenDelay, + "bgp.idle_hold": entry.IdleHold, + "bgp.prefix_limit": entry.PrefixLimit, + "bgp.holdtime": entry.Holdtime, + "bgp.holdtime_config": entry.HoldtimeConfig, + "bgp.keepalive": entry.Keepalive, + "bgp.keepalive_config": entry.KeepaliveConfig, + "bgp.msg_update_in": entry.MsgUpdateIn, + "bgp.msg_update_out": entry.MsgUpdateOut, + "bgp.msg_total_in": entry.MsgTotalIn, + "bgp.msg_total_out": entry.MsgTotalOut, + "bgp.last_update_age": entry.LastUpdateAge, + "bgp.last_error": entry.LastError, + "bgp.status_flap_counts": entry.StatusFlapCounts, + "bgp.established_counts": entry.EstablishedCounts, + "bgp.orf_entry_received": entry.ORFEntryReceived, + "bgp.nexthop_self": booleanFields["bgp.nexthop_self"], + "bgp.nexthop_thirdparty": booleanFields["bgp.nexthop_thirdparty"], + "bgp.nexthop_peer": booleanFields["bgp.nexthop_peer"], + }, + RootFields: rootFields, + } + + events = append(events, event) + } + + return events +} diff --git a/x-pack/metricbeat/module/panw/routing/routing.go b/x-pack/metricbeat/module/panw/routing/routing.go new file mode 100644 index 00000000000..ca762c25fef --- /dev/null +++ b/x-pack/metricbeat/module/panw/routing/routing.go @@ -0,0 +1,98 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package routing + +import ( + "errors" + "fmt" + + "github.com/elastic/beats/v7/libbeat/common/cfgwarn" + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + "github.com/elastic/elastic-agent-libs/logp" +) + +const ( + metricsetName = "routing" +) + +// MetricSet holds any configuration or state information. It must implement +// the mb.MetricSet interface. And this is best achieved by embedding +// mb.BaseMetricSet because it implements all of the required mb.MetricSet +// interface methods except for Fetch. +type MetricSet struct { + mb.BaseMetricSet + config *panw.Config + logger *logp.Logger + client panw.PanwClient +} + +// init registers the MetricSet with the central registry as soon as the program +// starts. The New function will be called later to instantiate an instance of +// the MetricSet for each host is defined in the module's configuration. After the +// MetricSet has been created then Fetch will begin to be called periodically. +func init() { + mb.Registry.MustAddMetricSet(panw.ModuleName, metricsetName, New) +} + +// New creates a new instance of the MetricSet. New is responsible for unpacking +// any MetricSet specific configuration options if there are any. +func New(base mb.BaseMetricSet) (mb.MetricSet, error) { + cfgwarn.Beta("The panw routing metricset is beta.") + + config, err := panw.NewConfig(base) + if err != nil { + return nil, err + } + + logger := logp.NewLogger(base.FullyQualifiedName()) + + client, err := panw.GetPanwClient(config) + if err != nil { + return nil, err + } + + return &MetricSet{ + BaseMetricSet: base, + config: config, + logger: logger, + client: client, + }, nil +} + +// Fetch method implements the data gathering and data conversion to the right +// format. It publishes the event which is then forwarded to the output. In case +// of an error set the Error field of mb.Event or simply call report.Error(). +func (m *MetricSet) Fetch(report mb.ReporterV2) error { + // accumulate errs and report them all at the end so that we don't + // stop processing events if one of the fetches fails + var errs []error + + eventFetchers := []struct { + name string + fn func(*MetricSet) ([]mb.Event, error) + }{ + {"bgp peers", getBGPEvents}, + } + + for _, fetcher := range eventFetchers { + events, err := fetcher.fn(m) + if err != nil { + m.logger.Errorf("Error getting %s events: %s", fetcher.name, err) + errs = append(errs, err) + } else { + for _, event := range events { + report.Event(event) + } + } + } + + if len(errs) > 0 { + return fmt.Errorf("error while fetching routing metrics: %w", errors.Join(errs...)) + } + + return nil + +} diff --git a/x-pack/metricbeat/module/panw/routing/routing_types.go b/x-pack/metricbeat/module/panw/routing/routing_types.go new file mode 100644 index 00000000000..1b39751edb8 --- /dev/null +++ b/x-pack/metricbeat/module/panw/routing/routing_types.go @@ -0,0 +1,83 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package routing + +type BGPResponse struct { + Status string `xml:"status,attr"` + Result BGPResult `xml:"result"` +} + +type BGPResult struct { + Entries []BGPEntry `xml:"entry"` +} + +type BGPEntry struct { + Peer string `xml:"peer,attr"` + Vr string `xml:"vr,attr"` + PeerGroup string `xml:"peer-group"` + PeerRouterID string `xml:"peer-router-id"` + RemoteAS int `xml:"remote-as"` + Status string `xml:"status"` + StatusDuration int `xml:"status-duration"` + PasswordSet string `xml:"password-set"` + Passive string `xml:"passive"` + MultiHopTTL int `xml:"multi-hop-ttl"` + PeerAddress string `xml:"peer-address"` + LocalAddress string `xml:"local-address"` + ReflectorClient string `xml:"reflector-client"` + SameConfederation string `xml:"same-confederation"` + AggregateConfedAS string `xml:"aggregate-confed-as"` + PeeringType string `xml:"peering-type"` + ConnectRetryInterval int `xml:"connect-retry-interval"` + OpenDelay int `xml:"open-delay"` + IdleHold int `xml:"idle-hold"` + PrefixLimit int `xml:"prefix-limit"` + Holdtime int `xml:"holdtime"` + HoldtimeConfig int `xml:"holdtime-config"` + Keepalive int `xml:"keepalive"` + KeepaliveConfig int `xml:"keepalive-config"` + MsgUpdateIn int `xml:"msg-update-in"` + MsgUpdateOut int `xml:"msg-update-out"` + MsgTotalIn int `xml:"msg-total-in"` + MsgTotalOut int `xml:"msg-total-out"` + LastUpdateAge int `xml:"last-update-age"` + LastError string `xml:"last-error"` + StatusFlapCounts int `xml:"status-flap-counts"` + EstablishedCounts int `xml:"established-counts"` + ORFEntryReceived int `xml:"ORF-entry-received"` + NexthopSelf string `xml:"nexthop-self"` + NexthopThirdparty string `xml:"nexthop-thirdparty"` + NexthopPeer string `xml:"nexthop-peer"` + Config BGPConfig `xml:"config"` + PeerCapability BGPPeerCapability `xml:"peer-capability"` + PrefixCounter BGPPrefixCounter `xml:"prefix-counter"` +} + +type BGPConfig struct { + RemovePrivateAS string `xml:"remove-private-as"` +} + +type BGPPeerCapability struct { + List []BGPCapability `xml:"list"` +} + +type BGPCapability struct { + Capability string `xml:"capability"` + Value string `xml:"value"` +} + +type BGPPrefixCounter struct { + Entries []BGPPrefixEntry `xml:"entry"` +} + +type BGPPrefixEntry struct { + AfiSafi string `xml:"afi-safi,attr"` + IncomingTotal int `xml:"incoming-total"` + IncomingAccepted int `xml:"incoming-accepted"` + IncomingRejected int `xml:"incoming-rejected"` + PolicyRejected int `xml:"policy-rejected"` + OutgoingTotal int `xml:"outgoing-total"` + OutgoingAdvertised int `xml:"outgoing-advertised"` +} diff --git a/x-pack/metricbeat/module/panw/system/_meta/data.json b/x-pack/metricbeat/module/panw/system/_meta/data.json new file mode 100644 index 00000000000..c5cf6da116f --- /dev/null +++ b/x-pack/metricbeat/module/panw/system/_meta/data.json @@ -0,0 +1,19 @@ +{ + "@timestamp":"2016-05-23T08:05:34.853Z", + "beat":{ + "hostname":"beathost", + "name":"beathost" + }, + "metricset":{ + "host":"localhost", + "module":"panw", + "name":"system", + "rtt":44269 + }, + "panw":{ + "system":{ + "example": "system" + } + }, + "type":"metricsets" +} diff --git a/x-pack/metricbeat/module/panw/system/_meta/docs.asciidoc b/x-pack/metricbeat/module/panw/system/_meta/docs.asciidoc new file mode 100644 index 00000000000..9fa06d4fca8 --- /dev/null +++ b/x-pack/metricbeat/module/panw/system/_meta/docs.asciidoc @@ -0,0 +1 @@ +This is the system metricset of the module panw. diff --git a/x-pack/metricbeat/module/panw/system/_meta/fields.yml b/x-pack/metricbeat/module/panw/system/_meta/fields.yml new file mode 100644 index 00000000000..87e8b542706 --- /dev/null +++ b/x-pack/metricbeat/module/panw/system/_meta/fields.yml @@ -0,0 +1,7 @@ +- name: system + type: group + release: beta + fields: + - name: example + type: keyword + dimension: true \ No newline at end of file diff --git a/x-pack/metricbeat/module/panw/system/certificates.go b/x-pack/metricbeat/module/panw/system/certificates.go new file mode 100644 index 00000000000..302225847b6 --- /dev/null +++ b/x-pack/metricbeat/module/panw/system/certificates.go @@ -0,0 +1,149 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package system + +import ( + "encoding/xml" + "errors" + "fmt" + "regexp" + "strings" + "time" + + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + "github.com/elastic/elastic-agent-libs/mapstr" +) + +const certificatesQuery = "" + +func getCertificateEvents(m *MetricSet) ([]mb.Event, error) { + + var response CertificateResponse + + output, err := m.client.Op(certificatesQuery, panw.Vsys, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to execute query: %w", err) + } + + err = xml.Unmarshal(output, &response) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal XML response: %w", err) + } + + if response.Result == "" { + return nil, fmt.Errorf("empty result from XML response") + } + + events, err := formatCertificateEvents(m, response.Result) + if err != nil { + return nil, fmt.Errorf("failed to format certificate events: %w", err) + } + + return events, nil +} + +func formatCertificateEvents(m *MetricSet, input string) ([]mb.Event, error) { + timestamp := time.Now().UTC() + rootFields := panw.MakeRootFields(m.config.HostIp) + + certificates, err := parseCertificates(input) + if err != nil { + return nil, err + } + + events := make([]mb.Event, 0, len(certificates)) + + for _, certificate := range certificates { + event := mb.Event{ + Timestamp: timestamp, + MetricSetFields: mapstr.M{ + "certificate.name": certificate.CertName, + "certificate.issuer": certificate.Issuer, + "certificate.issuer_subject_hash": certificate.IssuerSubjectHash, + "certificate.issuer_key_hash": certificate.IssuerKeyHash, + "certificate.db_type": certificate.DBType, + "certificate.db_exp_date": certificate.DBExpDate, + "certificate.db_rev_date": certificate.DBRevDate, + "certificate.db_serial_no": certificate.DBSerialNo, + "certificate.db_file": certificate.DBFile, + "certificate.db_name": certificate.DBName, + "certificate.db_status": certificate.DBStatus, + }, + RootFields: rootFields, + } + + events = append(events, event) + } + + return events, nil +} + +const ( + issuerPrefix = "issuer: " + issuerSubjectHashPrefix = "issuer-subjecthash: " + issuerKeyHashPrefix = "issuer-keyhash: " + dbTypePrefix = "db-type: " + dbExpDatePrefix = "db-exp-date: " + dbRevDatePrefix = "db-rev-date: " + dbSerialNoPrefix = "db-serialno: " + dbFilePrefix = "db-file: " + dbNamePrefix = "db-name: " + dbStatusPrefix = "db-status: " +) + +func parseCertificates(input string) ([]Certificate, error) { + lines := strings.Split(input, "\n") + pattern := `^[0-9A-Fa-f]{1,40}:[0-9A-Fa-f]{40}([0-9A-Fa-f]{24})?$` + regex := regexp.MustCompile(pattern) + + certificates := make([]Certificate, 0) + var currentSN Certificate + + for _, line := range lines { + line = strings.TrimSpace(line) + switch { + case regex.MatchString(line): + if currentSN.CertName != "" { + certificates = append(certificates, currentSN) + currentSN = Certificate{} + } + currentSN.CertName = line + case strings.HasPrefix(line, issuerPrefix): + currentSN.Issuer = strings.TrimPrefix(line, issuerPrefix) + case strings.HasPrefix(line, issuerSubjectHashPrefix): + currentSN.IssuerSubjectHash = strings.TrimPrefix(line, issuerSubjectHashPrefix) + case strings.HasPrefix(line, issuerKeyHashPrefix): + currentSN.IssuerKeyHash = strings.TrimPrefix(line, issuerKeyHashPrefix) + if strings.HasPrefix(currentSN.IssuerKeyHash, issuerKeyHashPrefix) { + currentSN.IssuerKeyHash = "" + } + case strings.HasPrefix(line, dbTypePrefix): + currentSN.DBType = strings.TrimPrefix(line, dbTypePrefix) + case strings.HasPrefix(line, dbExpDatePrefix): + currentSN.DBExpDate = strings.TrimPrefix(line, dbExpDatePrefix) + case strings.HasPrefix(line, dbRevDatePrefix): + currentSN.DBRevDate = strings.TrimPrefix(line, dbRevDatePrefix) + case strings.HasPrefix(line, dbSerialNoPrefix): + currentSN.DBSerialNo = strings.TrimPrefix(line, dbSerialNoPrefix) + case strings.HasPrefix(line, dbFilePrefix): + currentSN.DBFile = strings.TrimPrefix(line, dbFilePrefix) + case strings.HasPrefix(line, dbNamePrefix): + currentSN.DBName = strings.TrimPrefix(line, dbNamePrefix) + case strings.HasPrefix(line, dbStatusPrefix): + currentSN.DBStatus = strings.TrimPrefix(line, dbStatusPrefix) + } + } + + if currentSN.CertName != "" { + certificates = append(certificates, currentSN) + } + + if len(certificates) == 0 { + return nil, errors.New("no valid certificates found") + } + + return certificates, nil +} diff --git a/x-pack/metricbeat/module/panw/system/fans.go b/x-pack/metricbeat/module/panw/system/fans.go new file mode 100644 index 00000000000..e2c305c4524 --- /dev/null +++ b/x-pack/metricbeat/module/panw/system/fans.go @@ -0,0 +1,68 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package system + +import ( + "encoding/xml" + "fmt" + "time" + + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + "github.com/elastic/elastic-agent-libs/mapstr" +) + +const fansQuery = "" + +func getFanEvents(m *MetricSet) ([]mb.Event, error) { + + var response FanResponse + + output, err := m.client.Op(fansQuery, panw.Vsys, nil, nil) + if err != nil { + return nil, fmt.Errorf("error querying fan data: %w", err) + } + + err = xml.Unmarshal(output, &response) + if err != nil { + return nil, fmt.Errorf("error unmarshaling fan data: %w", err) + } + + return formatFanEvents(m, &response), nil +} + +func formatFanEvents(m *MetricSet, response *FanResponse) []mb.Event { + if response == nil || len(response.Result.Fan.Slots) == 0 { + return nil + } + + events := make([]mb.Event, 0) + timestamp := time.Now().UTC() + rootFields := panw.MakeRootFields(m.config.HostIp) + + for _, slot := range response.Result.Fan.Slots { + for _, entry := range slot.Entries { + alarm, err := panw.StringToBool(entry.Alarm) + if err != nil { + m.logger.Warn("Failed to convert alarm value %s to boolean: %s. Defaulting to false.", entry.Alarm, err) + } + m.Logger().Debugf("Processing slot %d entry %+v", entry.Slot, entry) + event := mb.Event{ + Timestamp: timestamp, + MetricSetFields: mapstr.M{ + "fan.slot_number": entry.Slot, + "fan.description": entry.Description, + "fan.alarm": alarm, + "fan.rpm": entry.RPMs, + "fan.min_rpm": entry.Min, + }, + RootFields: rootFields, + } + events = append(events, event) + } + } + + return events +} diff --git a/x-pack/metricbeat/module/panw/system/filesystem.go b/x-pack/metricbeat/module/panw/system/filesystem.go new file mode 100644 index 00000000000..205fed3f6c0 --- /dev/null +++ b/x-pack/metricbeat/module/panw/system/filesystem.go @@ -0,0 +1,156 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package system + +import ( + "encoding/xml" + "fmt" + "strconv" + "strings" + "time" + + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + "github.com/elastic/elastic-agent-libs/logp" + "github.com/elastic/elastic-agent-libs/mapstr" +) + +const ( + KBytes = 1024 + MBytes = 1024 * KBytes + GBytes = 1024 * MBytes +) + +const filesystemQuery = "" + +var filesystemLogger *logp.Logger + +func getFilesystemEvents(m *MetricSet) ([]mb.Event, error) { + // Set logger so all the parse functions have access + filesystemLogger = m.logger + var response FilesystemResponse + + output, err := m.client.Op(filesystemQuery, panw.Vsys, nil, nil) + if err != nil { + return nil, fmt.Errorf("error querying filesystem info: %w", err) + } + + if len(output) == 0 { + return nil, fmt.Errorf("received empty output from filesystem query") + } + + err = xml.Unmarshal(output, &response) + if err != nil { + return nil, fmt.Errorf("error unmarshaling filesystem response: %w", err) + } + + filesystems := getFilesystems(response.Result.Data) + events := formatFilesystemEvents(m, filesystems) + + return events, nil +} + +func getFilesystems(input string) []Filesystem { + + filesystemLogger.Debugf("getFilesystems input:\n %s", input) + lines := strings.Split(input, "\n") + filesystems := make([]Filesystem, 0, len(lines)-1) + + // Skip the first line which is the header: + // + // Example: + // Result from the XML API call is basically a command in Linux distribution i.e., "df -h"'s output: + // + // Filesystem Size Used Avail Use% Mounted on + // /dev/root 9.5G 4.0G 5.1G 44% / + // none 2.5G 64K 2.5G 1% /dev + // /dev/sda5 19G 9.1G 9.0G 51% /opt/pancfg + // + for _, line := range lines[1:] { + fields := strings.Fields(line) + if len(fields) == 6 { + filesystem := Filesystem{ + Name: fields[0], + Size: fields[1], + Used: fields[2], + Avail: fields[3], + UsePerc: fields[4], + Mounted: fields[5], + } + filesystems = append(filesystems, filesystem) + } + } + return filesystems +} + +func convertToBytes(field string, value string) float64 { + if len(value) == 0 { + filesystemLogger.Warn("convertToBytes called with empty value") + return -1 + } + + // value, for instance for "used", can be just "0", so just return that + if value == "0" { + return 0 + } + + //filesystemLogger.Warnf("convertToBytes field %s, value: %s.", field, value) + numstr := value[:len(value)-1] + units := strings.ToLower(value[len(value)-1:]) + result, err := strconv.ParseFloat(numstr, 32) + if err != nil { + filesystemLogger.Warnf("parseFloat failed to parse field %s, value: %s. Error: %v", field, value, err) + return -1 + } + + switch units { + case "k": + return result * KBytes + case "m": + return result * MBytes + case "g": + return result * GBytes + case "": + return result + default: + filesystemLogger.Warnf("Unhandled units for field %s, value %s: %s", field, value, units) + return result + } + +} + +func formatFilesystemEvents(m *MetricSet, filesystems []Filesystem) []mb.Event { + if len(filesystems) == 0 { + return nil + } + + events := make([]mb.Event, 0, len(filesystems)) + timestamp := time.Now().UTC() + rootFields := panw.MakeRootFields(m.config.HostIp) + + for _, filesystem := range filesystems { + used, err := strconv.ParseInt(filesystem.UsePerc[:len(filesystem.UsePerc)-1], 10, 64) + if err != nil { + filesystemLogger.Warnf("Failed to parse used percent: %v", err) + } + + event := mb.Event{ + Timestamp: timestamp, + MetricSetFields: mapstr.M{ + "filesystem.name": filesystem.Name, + "filesystem.size": convertToBytes("filesystem.size", filesystem.Size), + "filesystem.used": convertToBytes("filesystem.used", filesystem.Used), + "filesystem.available": convertToBytes("filesystem.available", filesystem.Avail), + "filesystem.use_percent": used, + "filesystem.mounted": filesystem.Mounted, + }, + RootFields: rootFields, + } + + events = append(events, event) + } + + return events +} diff --git a/x-pack/metricbeat/module/panw/system/license.go b/x-pack/metricbeat/module/panw/system/license.go new file mode 100644 index 00000000000..ee2df32c30d --- /dev/null +++ b/x-pack/metricbeat/module/panw/system/license.go @@ -0,0 +1,101 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package system + +import ( + "encoding/xml" + "fmt" + "strings" + "time" + + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + "github.com/elastic/elastic-agent-libs/mapstr" +) + +const ( + licenseQuery = "" + panwDateFormat = "January 2, 2006" +) + +func getLicenseEvents(m *MetricSet) ([]mb.Event, error) { + + var response LicenseResponse + + output, err := m.client.Op(licenseQuery, panw.Vsys, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to execute query: %w", err) + } + + if len(output) == 0 { + return nil, fmt.Errorf("empty response from PanOS for license query") + } + + err = xml.Unmarshal(output, &response) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal XML response: %w", err) + } + + if len(response.Result.Licenses) == 0 { + m.logger.Warn("No licenses found in the response") + return []mb.Event{}, nil + } + + return formatLicenseEvents(m, response.Result.Licenses), nil +} + +func formatLicenseEvents(m *MetricSet, licenses []License) []mb.Event { + events := make([]mb.Event, 0, len(licenses)) + timestamp := time.Now().UTC() + rootFields := panw.MakeRootFields(m.config.HostIp) + + for _, license := range licenses { + expired, err := panw.StringToBool(license.Expired) + if err != nil { + m.logger.Warn("Failed to convert expired value %s to boolean: %s. Defaulting to false.", license.Expired, err) + } + + // + // March 20, 2024 + // May 27, 2025 or Never + // + issued, err := time.Parse(panwDateFormat, license.Issued) + if err != nil { + m.logger.Warn("Failed to parse license issued date %s: %s", license.Issued, err) + } + neverExpires := false + expires, err := time.Parse(panwDateFormat, license.Expires) + // The value of license.Expires is "never" when the license never expires + if err != nil { + if strings.ToLower(license.Expires) == "never" { + neverExpires = true + } else { + m.logger.Warn("Failed to parse license expire date %s: %s", license.Expires, err) + } + } + + event := mb.Event{ + Timestamp: timestamp, + MetricSetFields: mapstr.M{ + "license.feature": license.Feature, + "license.description": license.Description, + "license.serial": license.Serial, + "license.issued": issued.Format(time.RFC3339), + "license.never_expires": neverExpires, + "license.expired": expired, + "license.auth_code": license.AuthCode, + }, + RootFields: rootFields, + } + // only set the expires field if the license expires + if !neverExpires { + event.MetricSetFields["license.expires"] = expires.Format(time.RFC3339) + } + + events = append(events, event) + } + + return events +} diff --git a/x-pack/metricbeat/module/panw/system/power.go b/x-pack/metricbeat/module/panw/system/power.go new file mode 100644 index 00000000000..50b89cf6ff0 --- /dev/null +++ b/x-pack/metricbeat/module/panw/system/power.go @@ -0,0 +1,69 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package system + +import ( + "encoding/xml" + "fmt" + "time" + + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + "github.com/elastic/elastic-agent-libs/mapstr" +) + +const powerQuery = "" + +// getPowerEvents retrieves power-related events from a PAN-OS device. +func getPowerEvents(m *MetricSet) ([]mb.Event, error) { + + var response PowerResponse + + output, err := m.client.Op(powerQuery, panw.Vsys, nil, nil) + if err != nil { + return nil, fmt.Errorf("failed to execute operation: %w", err) + } + + err = xml.Unmarshal(output, &response) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal XML response: %w", err) + } + + if len(response.Result.Power.Slots) == 0 { + m.logger.Warn("No power events found in the response") + return nil, nil + } + + events := formatPowerEvents(m, &response) + + return events, nil +} + +func formatPowerEvents(m *MetricSet, response *PowerResponse) []mb.Event { + events := make([]mb.Event, 0) + timestamp := time.Now().UTC() + rootFields := panw.MakeRootFields(m.config.HostIp) + + for _, slot := range response.Result.Power.Slots { + for _, entry := range slot.Entries { + m.Logger().Debugf("Processing slot %d entry %+v", entry.Slot, entry) + event := mb.Event{ + Timestamp: timestamp, + MetricSetFields: mapstr.M{ + "power.slot_number": entry.Slot, + "power.description": entry.Description, + "power.alarm": entry.Alarm, + "power.volts": entry.Volts, + "power.minimum_volts": entry.MinimumVolts, + "power.maximum_volts": entry.MaximumVolts, + }, + RootFields: rootFields, + } + events = append(events, event) + } + } + + return events +} diff --git a/x-pack/metricbeat/module/panw/system/resources.go b/x-pack/metricbeat/module/panw/system/resources.go new file mode 100644 index 00000000000..51c595b1fe0 --- /dev/null +++ b/x-pack/metricbeat/module/panw/system/resources.go @@ -0,0 +1,329 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package system + +import ( + "encoding/xml" + "fmt" + "regexp" + "strconv" + "strings" + "time" + + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + "github.com/elastic/elastic-agent-libs/logp" + "github.com/elastic/elastic-agent-libs/mapstr" +) + +const resourceQuery = "" + +var resourcesLogger *logp.Logger + +func getResourceEvents(m *MetricSet) ([]mb.Event, error) { + // Set logger so all the parse functions have access + resourcesLogger = m.logger + + var response ResourceResponse + output, err := m.client.Op(resourceQuery, panw.Vsys, nil, &response) + if err != nil { + return nil, fmt.Errorf("failed to execute operation: %w", err) + } + + err = xml.Unmarshal(output, &response) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal XML response: %w", err) + } + + events := formatResourceEvents(m, response.Result) + + return events, nil +} + +/* +Output from the XML API call is the standard "top" output: + +top - 07:51:37 up 108 days, 1:38, 0 users, load average: 5.52, 5.79, 5.99 +Tasks: 189 total, 7 running, 182 sleeping, 0 stopped, 0 zombie +%Cpu(s): 73.0 us, 4.6 sy, 0.0 ni, 21.7 id, 0.0 wa, 0.0 hi, 0.7 si, 0.0 st +MiB Mem : 5026.9 total, 414.2 free, 2541.5 used, 2071.1 buff/cache +MiB Swap: 5961.0 total, 4403.5 free, 1557.6 used. 1530.0 avail Mem + + PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND + 5692 20 0 121504 8396 6644 R 94.4 0.2 155491:08 pan_task + 5695 20 0 121504 7840 6632 R 94.4 0.2 155491:01 pan_task + 5696 20 0 121504 8360 6816 R 94.4 0.2 155486:29 pan_task + 5699 20 0 121504 8132 6676 R 94.4 0.2 155236:17 pan_task + 5700 20 0 146304 18424 6780 R 88.9 0.4 155491:40 pan_task + +22360 nobody 20 0 459836 40592 10148 R 22.2 0.8 0:38.65 httpd + + 6374 17 -3 1078156 18272 9716 S 5.6 0.4 215:46.41 routed + +14227 20 0 18108 7184 2172 R 5.6 0.1 0:00.04 top + + 1 20 0 2532 696 656 S 0.0 0.0 3:48.14 init + 2 20 0 0 0 0 S 0.0 0.0 0:00.83 kthreadd +*/ +func formatResourceEvents(m *MetricSet, input string) []mb.Event { + timestamp := time.Now().UTC() + rootFields := panw.MakeRootFields(m.config.HostIp) + events := make([]mb.Event, 0) + + // We only need the top 5 lines + lines := strings.Split(input, "\n") + lines = lines[:5] + + systemInfo := parseSystemInfo(lines[0]) + taskInfo := parseTaskInfo(lines[1]) + cpuInfo := parseCPUInfo(lines[2]) + memoryInfo := parseMemoryInfo(lines[3]) + swapInfo := parseSwapInfo(lines[4]) + + event := mb.Event{ + Timestamp: timestamp, + MetricSetFields: mapstr.M{ + "uptime": mapstr.M{ + "days": systemInfo.Uptime.Days, + "hours": systemInfo.Uptime.Hours, + "minutes": systemInfo.Uptime.Minutes, + }, + "user_count": systemInfo.UserCount, + "load_average": mapstr.M{ + "1m": systemInfo.LoadAverage.OneMinute, + "5m": systemInfo.LoadAverage.FiveMinute, + "15m": systemInfo.LoadAverage.FifteenMinute, + }, + "tasks": mapstr.M{ + "total": taskInfo.Total, + "running": taskInfo.Running, + "sleeping": taskInfo.Sleeping, + "stopped": taskInfo.Stopped, + "zombie": taskInfo.Zombie, + }, + "cpu": mapstr.M{ + "user": cpuInfo.User, + "system": cpuInfo.System, + "nice": cpuInfo.Nice, + "idle": cpuInfo.Idle, + "wait": cpuInfo.Wait, + "hi": cpuInfo.Hi, + "system_int": cpuInfo.SystemInt, + "steal": cpuInfo.Steal, + }, + "memory": mapstr.M{ + "total": memoryInfo.Total, + "free": memoryInfo.Free, + "used": memoryInfo.Used, + "buffer_cache": memoryInfo.BufferCache, + }, + "swap": mapstr.M{ + "total": swapInfo.Total, + "free": swapInfo.Free, + "used": swapInfo.Used, + "available": swapInfo.Available, + }, + }, + RootFields: rootFields, + } + + events = append(events, event) + return events +} + +func parseLoadAverage(line string) SystemLoad { + reLoadAvg := regexp.MustCompile(`load average:\s+([\d.]+),\s+([\d.]+),\s+([\d.]+)`) + var load1, load5, load15 float64 + + if matches := reLoadAvg.FindStringSubmatch(line); matches != nil { + load1 = parseFloat("load1", matches[1]) + load5 = parseFloat("load5", matches[2]) + load15 = parseFloat("load15", matches[3]) + } + + return SystemLoad{load1, load5, load15} +} + +func parseUserCount(line string) int { + reUserCount := regexp.MustCompile(`(\d+)\s+user`) + var userCount int + if matches := reUserCount.FindStringSubmatch(line); matches != nil { + userCount = parseInt("userCount", matches[1]) + } + + return userCount +} + +func parseUptime(line string) Uptime { + // Uptime less than 1 hour + // top - 15:03:02 up 4 min, 1 user, load average: 1.77, 2.74, 1.34 + // Uptime less than 1 day + // top - 16:16:29 up 1:18, 1 user, load average: 0.00, 0.02, 0.01 + // Uptime 1 day or more + // top - 11:08:26 up 2 days, 23:02, 1 user, load average: 0.40, 0.23, 0.37 + + // Regular expressions to match different uptime formats + // up < 1 hour + reMin := regexp.MustCompile(`up\s+(\d+)\s+min`) + // up >= 1 hour < 1 day + reHourMin := regexp.MustCompile(`up\s+(\d+):(\d+)`) + // up >= 1 day + reDayHourMin := regexp.MustCompile(`up\s+(\d+)\s+days?,\s+(\d+):(\d+)`) + + var days, hours, minutes int + var matches []string + + if matches = reMin.FindStringSubmatch(line); matches != nil { + minutes = parseInt("minutes", matches[1]) + } else if matches = reHourMin.FindStringSubmatch(line); matches != nil { + hours = parseInt("hours", matches[1]) + minutes = parseInt("minutes", matches[2]) + } else if matches = reDayHourMin.FindStringSubmatch(line); matches != nil { + days = parseInt("days", matches[1]) + hours = parseInt("hours", matches[2]) + minutes = parseInt("minutes", matches[3]) + } + + if matches == nil { + resourcesLogger.Errorf("Failed to parse uptime: %s", line) + return Uptime{} + } + + return Uptime{days, hours, minutes} +} + +func parseSystemInfo(line string) SystemInfo { + + uptime := parseUptime(line) + users := parseUserCount(line) + SystemLoad := parseLoadAverage(line) + + return SystemInfo{ + Uptime: uptime, + UserCount: users, + LoadAverage: SystemLoad, + } +} + +func parseTaskInfo(line string) TaskInfo { + //Tasks: 189 total, 7 running, 182 sleeping, 0 stopped, 0 zombie + + re := regexp.MustCompile(`Tasks:\s*(\d+)\s*total,\s*(\d+)\s*running,\s*(\d+)\s*sleeping,\s*(\d+)\s*stopped,\s*(\d+)\s*zombie`) + matches := re.FindStringSubmatch(line) + if matches == nil { + resourcesLogger.Errorf("Failed to parse task info: %s", line) + return TaskInfo{} + } + + total := parseInt("total", matches[1]) + running := parseInt("running", matches[2]) + sleeping := parseInt("sleeping", matches[3]) + stopped := parseInt("stopped", matches[4]) + zombie := parseInt("zombie", matches[5]) + + return TaskInfo{ + Total: total, + Running: running, + Sleeping: sleeping, + Stopped: stopped, + Zombie: zombie, + } +} + +func parseCPUInfo(line string) CPUInfo { + //%Cpu(s): 73.0 us, 4.6 sy, 0.0 ni, 21.7 id, 0.0 wa, 0.0 hi, 0.7 si, 0.0 st + re := regexp.MustCompile(`(\d+\.\d+)\s+us,\s+(\d+\.\d+)\s+sy,\s+(\d+\.\d+)\s+ni,\s+(\d+\.\d+)\s+id,\s+(\d+\.\d+)\s+wa,\s+(\d+\.\d+)\s+hi,\s+(\d+\.\d+)\s+si,\s+(\d+\.\d+)\s+st`) + matches := re.FindStringSubmatch(line) + if matches == nil { + resourcesLogger.Errorf("Failed to parse CPU info: %s", line) + return CPUInfo{} + } + + user := parseFloat("user", matches[1]) + system := parseFloat("system", matches[2]) + nice := parseFloat("nice", matches[3]) + idle := parseFloat("idle", matches[4]) + wait := parseFloat("wait", matches[5]) + hi := parseFloat("hi", matches[6]) + sysint := parseFloat("sysint", matches[7]) + steal := parseFloat("steal", matches[8]) + + return CPUInfo{ + User: user, + System: system, + Nice: nice, + Idle: idle, + Wait: wait, + Hi: hi, + SystemInt: sysint, + Steal: steal, + } +} + +func parseMemoryInfo(line string) MemoryInfo { + //MiB Mem : 5026.9 total, 414.2 free, 2541.5 used, 2071.1 buff/cache + re := regexp.MustCompile(`(\d+\.\d+)\s+total,\s+(\d+\.\d+)\s+free,\s+(\d+\.\d+)\s+used,\s+(\d+\.\d+)\s+buff/cache`) + matches := re.FindStringSubmatch(line) + if matches == nil { + resourcesLogger.Errorf("Failed to parse memory info: %s", line) + return MemoryInfo{} + } + + total := parseFloat("total", matches[1]) + free := parseFloat("free", matches[2]) + used := parseFloat("used", matches[3]) + bufferCache := parseFloat("bufferCache", matches[4]) + + return MemoryInfo{ + Total: total, + Free: free, + Used: used, + BufferCache: bufferCache, + } +} + +func parseSwapInfo(line string) SwapInfo { + //MiB Swap: 5961.0 total, 4403.5 free, 1557.6 used. 1530.0 avail Mem + // Note: the punctuation after the "used" is a ".", not a "," + re := regexp.MustCompile(`(\d+\.\d+)\s+total,\s+(\d+\.\d+)\s+free,\s+(\d+\.\d+)\s+used[,\.].\s+(\d+\.\d+)\s+avail Mem`) + matches := re.FindStringSubmatch(line) + + if matches == nil { + resourcesLogger.Errorf("Failed to parse swap info: %s", line) + return SwapInfo{} + } + + total := parseFloat("total", matches[1]) + free := parseFloat("free", matches[2]) + used := parseFloat("used", matches[3]) + available := parseFloat("available", matches[4]) + + return SwapInfo{ + Total: total, + Free: free, + Used: used, + Available: available, + } +} + +func parseFloat(field string, value string) float64 { + var result float64 + result, err := strconv.ParseFloat(value, 64) + if err != nil { + resourcesLogger.Errorf("parseFloat failed to parse field %s: %v", field, err) + return -1 + } + + return result +} + +func parseInt(field string, value string) int { + result, err := strconv.Atoi(value) + if err != nil { + resourcesLogger.Errorf("parseInt failed to parse field %s: %v", field, err) + return -1 + } + return result +} diff --git a/x-pack/metricbeat/module/panw/system/system.go b/x-pack/metricbeat/module/panw/system/system.go new file mode 100644 index 00000000000..085af03d82e --- /dev/null +++ b/x-pack/metricbeat/module/panw/system/system.go @@ -0,0 +1,106 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package system + +import ( + "errors" + "fmt" + + "github.com/elastic/beats/v7/libbeat/common/cfgwarn" + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + "github.com/elastic/elastic-agent-libs/logp" +) + +const ( + metricsetName = "system" + vsys = "" +) + +// MetricSet holds any configuration or state information. It must implement +// the mb.MetricSet interface. And this is best achieved by embedding +// mb.BaseMetricSet because it implements all of the required mb.MetricSet +// interface methods except for Fetch. +type MetricSet struct { + mb.BaseMetricSet + config *panw.Config + logger *logp.Logger + client panw.PanwClient +} + +// init registers the MetricSet with the central registry as soon as the program +// starts. The New function will be called later to instantiate an instance of +// the MetricSet for each host is defined in the module's configuration. After the +// MetricSet has been created then Fetch will begin to be called periodically. +func init() { + mb.Registry.MustAddMetricSet(panw.ModuleName, metricsetName, New) +} + +// New creates a new instance of the MetricSet. New is responsible for unpacking +// any MetricSet specific configuration options if there are any. +func New(base mb.BaseMetricSet) (mb.MetricSet, error) { + cfgwarn.Beta("The panw system metricset is beta.") + + config, err := panw.NewConfig(base) + if err != nil { + return nil, err + } + + logger := logp.NewLogger(base.FullyQualifiedName()) + + //client := &pango.Firewall{Client: pango.Client{Hostname: config.HostIp, ApiKey: config.ApiKey}} + client, err := panw.GetPanwClient(config) + if err != nil { + return nil, err + } + + return &MetricSet{ + BaseMetricSet: base, + config: config, + logger: logger, + client: client, + }, nil +} + +// Fetch method implements the data gathering and data conversion to the right +// format. It publishes the event which is then forwarded to the output. In case +// of an error set the Error field of mb.Event or simply call report.Error(). +func (m *MetricSet) Fetch(report mb.ReporterV2) error { + // accumulate errs and report them all at the end so that we don't + // stop processing events if one of the fetches fails + var errs []error + + eventFetchers := []struct { + name string + fn func(*MetricSet) ([]mb.Event, error) + }{ + {"certificates", getCertificateEvents}, + {"resources", getResourceEvents}, + {"power", getPowerEvents}, + {"fans", getFanEvents}, + {"thermal", getThermalEvents}, + {"licenses", getLicenseEvents}, + {"filesystem", getFilesystemEvents}, + } + + for _, fetcher := range eventFetchers { + events, err := fetcher.fn(m) + if err != nil { + m.logger.Errorf("Error getting %s events: %s", fetcher.name, err) + errs = append(errs, err) + } else { + for _, event := range events { + report.Event(event) + } + } + } + + if len(errs) > 0 { + return fmt.Errorf("error while fetching system metrics: %w", errors.Join(errs...)) + } + + return nil + +} diff --git a/x-pack/metricbeat/module/panw/system/system_types.go b/x-pack/metricbeat/module/panw/system/system_types.go new file mode 100644 index 00000000000..41adce4697b --- /dev/null +++ b/x-pack/metricbeat/module/panw/system/system_types.go @@ -0,0 +1,212 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package system + +import ( + "encoding/xml" +) + +// system resources +type ResourceResponse struct { + Status string `xml:"status,attr"` + Result string `xml:"result"` +} + +type SystemLoad struct { + OneMinute float64 + FiveMinute float64 + FifteenMinute float64 +} + +type Uptime struct { + Days int + Hours int + Minutes int +} + +type SystemInfo struct { + Uptime Uptime + UserCount int + LoadAverage SystemLoad +} + +type TaskInfo struct { + Total int + Running int + Sleeping int + Stopped int + Zombie int +} + +type CPUInfo struct { + User float64 + System float64 + Nice float64 + Idle float64 + Wait float64 + Hi float64 + SystemInt float64 + Steal float64 +} + +type MemoryInfo struct { + Total float64 + Free float64 + Used float64 + BufferCache float64 +} + +type SwapInfo struct { + Total float64 + Free float64 + Used float64 + Available float64 +} + +// temperature +type ThermalResponse struct { + Status string `xml:"status,attr"` + Result ThermalResult `xml:"result"` +} + +type ThermalResult struct { + Thermal Thermal `xml:"thermal"` +} + +type Thermal struct { + Slots []ThermalSlot `xml:",any"` +} + +type ThermalSlot struct { + Name xml.Name `xml:",any"` + Entries []ThermalEntry `xml:"entry"` +} + +type ThermalEntry struct { + Slot int `xml:"slot"` + Description string `xml:"description"` + Alarm string `xml:"alarm"` + DegreesCelsius float64 `xml:"DegreesC"` + MinimumTemp float64 `xml:"min"` + MaximumTemp float64 `xml:"max"` +} + +// power + +type PowerResponse struct { + Status string `xml:"status,attr"` + Result PowerResult `xml:"result"` +} + +type PowerResult struct { + Power Power `xml:"power"` +} + +type Power struct { + Slots []PowerSlot `xml:",any"` +} + +type PowerSlot struct { + Entries []PowerEntry `xml:"entry"` +} + +type PowerEntry struct { + Slot int `xml:"slot"` + Description string `xml:"description"` + Alarm bool `xml:"alarm"` + Volts float64 `xml:"Volts"` + MinimumVolts float64 `xml:"min"` + MaximumVolts float64 `xml:"max"` +} + +// fans + +type FanResponse struct { + Status string `xml:"status,attr"` + Result FanResult `xml:"result"` +} + +type FanResult struct { + Fan Fan `xml:"fan"` +} + +type Fan struct { + Slots []FanSlot `xml:",any"` +} + +type FanSlot struct { + Entries []FanEntry `xml:"entry"` +} + +type FanEntry struct { + Slot int `xml:"slot"` + Description string `xml:"description"` + Alarm string `xml:"alarm"` + RPMs int `xml:"RPMs"` + Min int `xml:"min"` +} + +// filesystem + +type FilesystemResponse struct { + XMLName xml.Name `xml:"response"` + Status string `xml:"status,attr"` + Result FilesystemResult `xml:"result"` +} + +type FilesystemResult struct { + Data string `xml:",cdata"` +} + +type Filesystem struct { + Name string + Size string + Used string + Avail string + UsePerc string + Mounted string +} + +// licenses + +type LicenseResponse struct { + Status string `xml:"status,attr"` + Result LicenseResult `xml:"result"` +} + +type LicenseResult struct { + Licenses []License `xml:"licenses>entry"` +} + +type License struct { + Feature string `xml:"feature"` + Description string `xml:"description"` + Serial string `xml:"serial"` + Issued string `xml:"issued"` + Expires string `xml:"expires"` + Expired string `xml:"expired"` + AuthCode string `xml:"authcode"` +} + +// certificates + +type CertificateResponse struct { + Status string `xml:"status,attr"` + Result string `xml:"result"` +} + +type Certificate struct { + CertName string + Issuer string + IssuerSubjectHash string + IssuerKeyHash string + DBType string + DBExpDate string + DBRevDate string + DBSerialNo string + DBFile string + DBName string + DBStatus string +} diff --git a/x-pack/metricbeat/module/panw/system/thermal.go b/x-pack/metricbeat/module/panw/system/thermal.go new file mode 100644 index 00000000000..7f74372eb25 --- /dev/null +++ b/x-pack/metricbeat/module/panw/system/thermal.go @@ -0,0 +1,76 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package system + +import ( + "encoding/xml" + "time" + + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + "github.com/elastic/elastic-agent-libs/mapstr" +) + +const thermalQuery = "" + +func getThermalEvents(m *MetricSet) ([]mb.Event, error) { + var response ThermalResponse + + output, err := m.client.Op(thermalQuery, panw.Vsys, nil, nil) + if err != nil { + m.logger.Error("Error: %s", err) + return nil, err + } + + err = xml.Unmarshal(output, &response) + if err != nil { + m.logger.Error("Error: %s", err) + return nil, err + } + + events := formatThermalEvents(m, &response) + + return events, nil + +} + +func formatThermalEvents(m *MetricSet, response *ThermalResponse) []mb.Event { + if response == nil || len(response.Result.Thermal.Slots) == 0 { + return nil + } + + events := make([]mb.Event, 0, len(response.Result.Thermal.Slots)) + timestamp := time.Now().UTC() + rootFields := panw.MakeRootFields(m.config.HostIp) + + var event mb.Event + + for _, slot := range response.Result.Thermal.Slots { + for _, entry := range slot.Entries { + alarm, err := panw.StringToBool(entry.Alarm) + if err != nil { + m.logger.Warn("Failed to convert alarm value %s to boolean: %s. Defaulting to false.", entry.Alarm, err) + } + m.logger.Debugf("Processing slot %d entry %+v", entry.Slot, entry) + event = mb.Event{ + Timestamp: timestamp, + MetricSetFields: mapstr.M{ + "thermal.slot_number": entry.Slot, + "thermal.description": entry.Description, + "thermal.alarm": alarm, + "thermal.degress_celsius": entry.DegreesCelsius, + "thermal.minimum_temp": entry.MinimumTemp, + "thermal.maximum_temp": entry.MaximumTemp, + }, + RootFields: rootFields, + } + + events = append(events, event) + + } + } + + return events +} diff --git a/x-pack/metricbeat/module/panw/util.go b/x-pack/metricbeat/module/panw/util.go new file mode 100644 index 00000000000..a1da2b51590 --- /dev/null +++ b/x-pack/metricbeat/module/panw/util.go @@ -0,0 +1,38 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package panw + +import ( + "fmt" + "strings" + + "github.com/elastic/elastic-agent-libs/mapstr" +) + +func StringToBool(s string) (bool, error) { + s = strings.ToLower(s) + switch s { + case "yes": + return true, nil + case "true": + return true, nil + case "no": + return false, nil + case "false": + return false, nil + } + + // Default to false + return false, fmt.Errorf("invalid value: %s", s) +} + +func MakeRootFields(HostIp string) mapstr.M { + return mapstr.M{ + "observer.ip": HostIp, + "host.ip": HostIp, + "observer.vendor": "Palo Alto", + "observer.type": "firewall", + } +} diff --git a/x-pack/metricbeat/module/panw/vpn/_meta/data.json b/x-pack/metricbeat/module/panw/vpn/_meta/data.json new file mode 100644 index 00000000000..f69cc32c300 --- /dev/null +++ b/x-pack/metricbeat/module/panw/vpn/_meta/data.json @@ -0,0 +1,19 @@ +{ + "@timestamp":"2016-05-23T08:05:34.853Z", + "beat":{ + "hostname":"beathost", + "name":"beathost" + }, + "metricset":{ + "host":"localhost", + "module":"panw", + "name":"vpn", + "rtt":44269 + }, + "panw":{ + "vpn":{ + "example": "vpn" + } + }, + "type":"metricsets" +} diff --git a/x-pack/metricbeat/module/panw/vpn/_meta/docs.asciidoc b/x-pack/metricbeat/module/panw/vpn/_meta/docs.asciidoc new file mode 100644 index 00000000000..73afb8e5982 --- /dev/null +++ b/x-pack/metricbeat/module/panw/vpn/_meta/docs.asciidoc @@ -0,0 +1 @@ +This is the vpn metricset of the module panw. diff --git a/x-pack/metricbeat/module/panw/vpn/_meta/fields.yml b/x-pack/metricbeat/module/panw/vpn/_meta/fields.yml new file mode 100644 index 00000000000..f6730c0a956 --- /dev/null +++ b/x-pack/metricbeat/module/panw/vpn/_meta/fields.yml @@ -0,0 +1,7 @@ +- name: vpn + type: group + release: beta + fields: + - name: example + type: keyword + dimension: true \ No newline at end of file diff --git a/x-pack/metricbeat/module/panw/vpn/gp_sessions.go b/x-pack/metricbeat/module/panw/vpn/gp_sessions.go new file mode 100644 index 00000000000..a82924f8a9c --- /dev/null +++ b/x-pack/metricbeat/module/panw/vpn/gp_sessions.go @@ -0,0 +1,89 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package vpn + +import ( + "encoding/xml" + "fmt" + "time" + + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + "github.com/elastic/elastic-agent-libs/mapstr" +) + +const gpSessionsQuery = "" + +func getGlobalProtectSessionEvents(m *MetricSet) ([]mb.Event, error) { + var response GPSessionsResponse + + output, err := m.client.Op(gpSessionsQuery, panw.Vsys, nil, nil) + if err != nil { + m.logger.Error("Error: %s", err) + return nil, fmt.Errorf("error querying GlobalProtect sessions: %w", err) + } + + err = xml.Unmarshal(output, &response) + if err != nil { + m.logger.Error("Error: %s", err) + return nil, fmt.Errorf("error unmarshaling GlobalProtect sessions response: %w", err) + } + + events := formatGPSessionEvents(m, response.Result.Sessions) + + return events, nil + +} + +func formatGPSessionEvents(m *MetricSet, sessions []GPSession) []mb.Event { + if len(sessions) == 0 { + return nil + } + + events := make([]mb.Event, 0, len(sessions)) + timestamp := time.Now().UTC() + rootFields := panw.MakeRootFields(m.config.HostIp) + + for _, session := range sessions { + isLocal, err := panw.StringToBool(session.IsLocal) + if err != nil { + m.logger.Warn("Failed to convert alarm value %s to boolean: %s. Defaulting to false.", session.IsLocal, err) + } + event := mb.Event{ + Timestamp: timestamp, + MetricSetFields: mapstr.M{ + "globalprotect.session.domain": session.Domain, + "globalprotect.session.is_local": isLocal, + "globalprotect.session.username": session.Username, + "globalprotect.session.primary_username": session.PrimaryUsername, + "globalprotect.session.region_for_config": session.RegionForConfig, + "globalprotect.session.source_region": session.SourceRegion, + "globalprotect.session.computer": session.Computer, + "globalprotect.session.client": session.Client, + "globalprotect.session.vpn_type": session.VPNType, + "globalprotect.session.host_id": session.HostID, + "globalprotect.session.app_version": session.AppVersion, + "globalprotect.session.virtual_ip": session.VirtualIP, + "globalprotect.session.virtual_ipv6": session.VirtualIPv6, + "globalprotect.session.public_ip": session.PublicIP, + "globalprotect.session.public_ipv6": session.PublicIPv6, + "globalprotect.session.tunnel_type": session.TunnelType, + "globalprotect.session.public_connection_ipv6": session.PublicConnectionIPv6, + "globalprotect.session.client_ip": session.ClientIP, + "globalprotect.session.login_time": session.LoginTime, + "globalprotect.session.login_time_utc": session.LoginTimeUTC, + "globalprotect.session.lifetime": session.Lifetime, + "globalprotect.session.request_login": session.RequestLogin, + "globalprotect.session.request_get_config": session.RequestGetConfig, + "globalprotect.session.request_sslvpn_connect": session.RequestSSLVPNConnect, + }, + RootFields: rootFields, + } + + events = append(events, event) + } + + return events +} diff --git a/x-pack/metricbeat/module/panw/vpn/gp_stats.go b/x-pack/metricbeat/module/panw/vpn/gp_stats.go new file mode 100644 index 00000000000..10b6a0cd729 --- /dev/null +++ b/x-pack/metricbeat/module/panw/vpn/gp_stats.go @@ -0,0 +1,71 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package vpn + +import ( + "encoding/xml" + "fmt" + "time" + + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + "github.com/elastic/elastic-agent-libs/mapstr" +) + +const gpStatsQuery = "" + +func getGlobalProtectStatsEvents(m *MetricSet) ([]mb.Event, error) { + + var response GPStatsResponse + + output, err := m.client.Op(gpStatsQuery, panw.Vsys, nil, nil) + if err != nil { + m.logger.Error("Error: %s", err) + return nil, fmt.Errorf("error querying GlobalProtect statistics: %w", err) + } + + err = xml.Unmarshal(output, &response) + if err != nil { + m.logger.Error("Error: %s", err) + return nil, fmt.Errorf("error unmarshaling GlobalProtect statistics response: %w", err) + } + + events := formatGPStatsEvents(m, response) + + return events, nil + +} + +func formatGPStatsEvents(m *MetricSet, response GPStatsResponse) []mb.Event { + + if len(response.Result.Gateways) == 0 { + return nil + } + + events := make([]mb.Event, 0, len(response.Result.Gateways)) + timestamp := time.Now().UTC() + rootFields := panw.MakeRootFields(m.config.HostIp) + + totalCurrent := response.Result.TotalCurrentUsers + totalPrevious := response.Result.TotalPreviousUsers + + for _, gateway := range response.Result.Gateways { + event := mb.Event{ + Timestamp: timestamp, + MetricSetFields: mapstr.M{ + "globalprotect.gateway.name": gateway.Name, + "globalprotect.gateway.current_users": gateway.CurrentUsers, + "globalprotect.gateway.previous_users": gateway.PreviousUsers, + "globalprotect.total_current_users": totalCurrent, + "globalprotect.total_previous_users": totalPrevious, + }, + RootFields: rootFields, + } + + events = append(events, event) + } + + return events +} diff --git a/x-pack/metricbeat/module/panw/vpn/vpn.go b/x-pack/metricbeat/module/panw/vpn/vpn.go new file mode 100644 index 00000000000..4a1f304b631 --- /dev/null +++ b/x-pack/metricbeat/module/panw/vpn/vpn.go @@ -0,0 +1,101 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package vpn + +import ( + "errors" + "fmt" + + "github.com/elastic/beats/v7/libbeat/common/cfgwarn" + "github.com/elastic/beats/v7/metricbeat/mb" + "github.com/elastic/beats/v7/x-pack/metricbeat/module/panw" + "github.com/elastic/elastic-agent-libs/logp" +) + +const ( + metricsetName = "vpn" + vsys = "" +) + +// MetricSet holds any configuration or state information. It must implement +// the mb.MetricSet interface. And this is best achieved by embedding +// mb.BaseMetricSet because it implements all of the required mb.MetricSet +// interface methods except for Fetch. +type MetricSet struct { + mb.BaseMetricSet + config *panw.Config + logger *logp.Logger + client panw.PanwClient +} + +// init registers the MetricSet with the central registry as soon as the program +// starts. The New function will be called later to instantiate an instance of +// the MetricSet for each host is defined in the module's configuration. After the +// MetricSet has been created then Fetch will begin to be called periodically. +func init() { + mb.Registry.MustAddMetricSet(panw.ModuleName, metricsetName, New) +} + +// New creates a new instance of the MetricSet. New is responsible for unpacking +// any MetricSet specific configuration options if there are any. +func New(base mb.BaseMetricSet) (mb.MetricSet, error) { + cfgwarn.Beta("The panw vpn metricset is beta.") + + config, err := panw.NewConfig(base) + if err != nil { + return nil, err + } + + logger := logp.NewLogger(base.FullyQualifiedName()) + + //client := &pango.Firewall{Client: pango.Client{Hostname: config.HostIp, ApiKey: config.ApiKey}} + client, err := panw.GetPanwClient(config) + if err != nil { + return nil, err + } + + return &MetricSet{ + BaseMetricSet: base, + config: config, + logger: logger, + client: client, + }, nil +} + +// Fetch method implements the data gathering and data conversion to the right +// format. It publishes the event which is then forwarded to the output. In case +// of an error set the Error field of mb.Event or simply call report.Error(). +func (m *MetricSet) Fetch(report mb.ReporterV2) error { + // accumulate errs and report them all at the end so that we don't + // stop processing events if one of the fetches fails + var errs []error + + eventFetchers := []struct { + name string + fn func(*MetricSet) ([]mb.Event, error) + }{ + {"globalprotect session", getGlobalProtectSessionEvents}, + {"globalprotect stats", getGlobalProtectStatsEvents}, + } + + for _, fetcher := range eventFetchers { + events, err := fetcher.fn(m) + if err != nil { + m.logger.Errorf("Error getting %s events: %s", fetcher.name, err) + errs = append(errs, err) + } else { + for _, event := range events { + report.Event(event) + } + } + } + + if len(errs) > 0 { + return fmt.Errorf("error while fetching vpn metrics: %w", errors.Join(errs...)) + } + + return nil + +} diff --git a/x-pack/metricbeat/module/panw/vpn/vpn_types.go b/x-pack/metricbeat/module/panw/vpn/vpn_types.go new file mode 100644 index 00000000000..216a6b77509 --- /dev/null +++ b/x-pack/metricbeat/module/panw/vpn/vpn_types.go @@ -0,0 +1,61 @@ +// Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one +// or more contributor license agreements. Licensed under the Elastic License; +// you may not use this file except in compliance with the Elastic License. + +package vpn + +// GlobalProtect sesssions +type GPSessionsResponse struct { + Status string `xml:"status,attr"` + Result GPSessionsResult `xml:"result"` +} + +type GPSessionsResult struct { + Sessions []GPSession `xml:"entry"` +} + +type GPSession struct { + Domain string `xml:"domain"` + IsLocal string `xml:"islocal"` + Username string `xml:"username"` + PrimaryUsername string `xml:"primary-username"` + RegionForConfig string `xml:"region-for-config"` + SourceRegion string `xml:"source-region"` + Computer string `xml:"computer"` + Client string `xml:"client"` + VPNType string `xml:"vpn-type"` + HostID string `xml:"host-id"` + AppVersion string `xml:"app-version"` + VirtualIP string `xml:"virtual-ip"` + VirtualIPv6 string `xml:"virtual-ipv6"` + PublicIP string `xml:"public-ip"` + PublicIPv6 string `xml:"public-ipv6"` + TunnelType string `xml:"tunnel-type"` + PublicConnectionIPv6 string `xml:"public-connection-ipv6"` + ClientIP string `xml:"client-ip"` + LoginTime string `xml:"login-time"` + LoginTimeUTC string `xml:"login-time-utc"` + Lifetime string `xml:"lifetime"` + RequestLogin string `xml:"request-login"` + RequestGetConfig string `xml:"request-getconfig"` + RequestSSLVPNConnect string `xml:"request-sslvpnconnect"` +} + +// GlobalProtect gateway stats + +type GPStatsResponse struct { + Status string `xml:"status,attr"` + Result GPStatsResult `xml:"result"` +} + +type GPStatsResult struct { + Gateways []GPGateway `xml:"Gateway"` + TotalCurrentUsers int `xml:"TotalCurrentUsers"` + TotalPreviousUsers int `xml:"TotalPreviousUsers"` +} + +type GPGateway struct { + Name string `xml:"name"` + CurrentUsers int `xml:"CurrentUsers"` + PreviousUsers int `xml:"PreviousUsers"` +} diff --git a/x-pack/metricbeat/modules.d/panw.yml.disabled b/x-pack/metricbeat/modules.d/panw.yml.disabled new file mode 100644 index 00000000000..72242e7c7aa --- /dev/null +++ b/x-pack/metricbeat/modules.d/panw.yml.disabled @@ -0,0 +1,9 @@ +# Module: panw +# Docs: https://www.elastic.co/guide/en/beats/metricbeat/main/metricbeat-module-panw.html + +- module: panw + metricsets: ["licenses"] + enabled: false + period: 10s + hosts: ["localhost"] + From 88e30bbcf598be3a7be51fad133db63dfaa08b8a Mon Sep 17 00:00:00 2001 From: tommyers-elastic <106530686+tommyers-elastic@users.noreply.github.com> Date: Tue, 17 Sep 2024 21:47:13 +0100 Subject: [PATCH 2/2] fix merge issues --- go.mod | 6 +- x-pack/metricbeat/metricbeat.reference.yml | 64 ------------------- x-pack/metricbeat/modules.d/panw.yml.disabled | 2 +- 3 files changed, 2 insertions(+), 70 deletions(-) diff --git a/go.mod b/go.mod index 79ea2b827ba..9aecbae2439 100644 --- a/go.mod +++ b/go.mod @@ -189,6 +189,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/storage/azblob v1.4.0 github.com/Azure/azure-storage-blob-go v0.15.0 github.com/Azure/go-autorest/autorest/adal v0.9.24 + github.com/PaloAltoNetworks/pango v0.10.2 github.com/apache/arrow/go/v14 v14.0.2 github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.1 github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.16.15 @@ -396,12 +397,7 @@ require ( ) require ( -<<<<<<< HEAD cloud.google.com/go/storage v1.38.0 -======= - cloud.google.com/go/storage v1.43.0 - github.com/PaloAltoNetworks/pango v0.10.2 ->>>>>>> cc2c92571f (x-pack/metricbeat/module/panw: Add a new module (#40686)) github.com/dlclark/regexp2 v1.4.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/moby/term v0.5.0 // indirect diff --git a/x-pack/metricbeat/metricbeat.reference.yml b/x-pack/metricbeat/metricbeat.reference.yml index 764665119e9..6d3f6547bd4 100644 --- a/x-pack/metricbeat/metricbeat.reference.yml +++ b/x-pack/metricbeat/metricbeat.reference.yml @@ -1327,64 +1327,6 @@ metricbeat.modules: # Password to use when connecting to PostgreSQL. Empty by default. #password: pass -<<<<<<< HEAD -#------------------------------ Prometheus Module ------------------------------ -# Metrics collected from a Prometheus endpoint -- module: prometheus - period: 10s - metricsets: ["collector"] - hosts: ["localhost:9090"] - metrics_path: /metrics - #metrics_filters: - # include: [] - # exclude: [] - #username: "user" - #password: "secret" - - # This can be used for service account based authorization: - #bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token - #ssl.certificate_authorities: - # - /var/run/secrets/kubernetes.io/serviceaccount/service-ca.crt - - -# Metrics sent by a Prometheus server using remote_write option -#- module: prometheus -# metricsets: ["remote_write"] -# host: "localhost" -# port: "9201" - - # Secure settings for the server using TLS/SSL: - #ssl.certificate: "/etc/pki/server/cert.pem" - #ssl.key: "/etc/pki/server/cert.key" - -# Metrics that will be collected using a PromQL -#- module: prometheus -# metricsets: ["query"] -# hosts: ["localhost:9090"] -# period: 10s -# queries: -# - name: "instant_vector" -# path: "/api/v1/query" -# params: -# query: "sum(rate(prometheus_http_requests_total[1m]))" -# - name: "range_vector" -# path: "/api/v1/query_range" -# params: -# query: "up" -# start: "2019-12-20T00:00:00.000Z" -# end: "2019-12-21T00:00:00.000Z" -# step: 1h -# - name: "scalar" -# path: "/api/v1/query" -# params: -# query: "100" -# - name: "string" -# path: "/api/v1/query" -# params: -# query: "some_value" - -======= ->>>>>>> cc2c92571f (x-pack/metricbeat/module/panw: Add a new module (#40686)) #----------------------- Prometheus Typed Metrics Module ----------------------- - module: prometheus period: 10s @@ -1469,9 +1411,6 @@ metricbeat.modules: #username: "user" #password: "secret" - # Count number of metrics present in Elasticsearch document (default: false) - #metrics_count: false - # This can be used for service account based authorization: #bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token #ssl.certificate_authorities: @@ -1484,9 +1423,6 @@ metricbeat.modules: # host: "localhost" # port: "9201" - # Count number of metrics present in Elasticsearch document (default: false) - #metrics_count: false - # Secure settings for the server using TLS/SSL: #ssl.certificate: "/etc/pki/server/cert.pem" #ssl.key: "/etc/pki/server/cert.key" diff --git a/x-pack/metricbeat/modules.d/panw.yml.disabled b/x-pack/metricbeat/modules.d/panw.yml.disabled index 72242e7c7aa..c1643b4567c 100644 --- a/x-pack/metricbeat/modules.d/panw.yml.disabled +++ b/x-pack/metricbeat/modules.d/panw.yml.disabled @@ -1,5 +1,5 @@ # Module: panw -# Docs: https://www.elastic.co/guide/en/beats/metricbeat/main/metricbeat-module-panw.html +# Docs: https://www.elastic.co/guide/en/beats/metricbeat/8.15/metricbeat-module-panw.html - module: panw metricsets: ["licenses"]