From 07717a29048a127817ac6efcb7c8bc1b09aefab7 Mon Sep 17 00:00:00 2001 From: J Boddey Date: Thu, 28 Sep 2023 10:24:42 +0000 Subject: [PATCH 01/16] Merge dev into main (#117) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Implement test orchestrator (#4) * Initial work on test-orchestrator * Ignore runtime folder * Update runtime directory for test modules * Fix logging Add initial framework for running tests * logging and misc cleanup * logging changes * Add a stop hook after all tests complete * Refactor test_orc code * Add arg passing Add option to use locally cloned via install or remote via main project network orchestrator * Fix baseline module Fix orchestrator exiting only after timeout * Add result file to baseline test module Change result format to match closer to design doc * Refactor pylint * Skip test module if it failed to start * Refactor * Check for valid log level --------- Co-authored-by: Jacob Boddey * Add issue report templates (#7) * Add issue templates * Update README.md * Discover devices on the network (#5) * Test run sync (#8) * Initial work on test-orchestrator * Ignore runtime folder * Update runtime directory for test modules * Fix logging Add initial framework for running tests * logging and misc cleanup * logging changes * Add a stop hook after all tests complete * Refactor test_orc code * Add arg passing Add option to use locally cloned via install or remote via main project network orchestrator * Fix baseline module Fix orchestrator exiting only after timeout * Add result file to baseline test module Change result format to match closer to design doc * Refactor pylint * Skip test module if it failed to start * Refactor * Check for valid log level * Add config file arg Misc changes to network start procedure * fix merge issues * Update runner and test orch procedure Add useful runtiem args * Restructure test run startup process Misc updates to work with net orch updates * Refactor --------- * Quick refactor (#9) * Fix duplicate sleep calls * Add net orc (#11) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add the DNS test module (#12) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add dns test module Fix test module build process * Add mac address of device under test to test container Update dns test to use mac address filter * Update dns module tests * Change result output * logging update * Update test module for better reusability * Load in module config to test module * logging cleanup * Update baseline module to new template Misc cleanup * Add ability to disable individual tests * remove duplicate readme * Update device directories * Remove local folder * Update device template Update test module to work with new device config file format * Change test module network config options Do not start network services for modules not configured for network * Refactor --------- * Add baseline and pylint tests (#25) * Discover devices on the network (#22) * Discover devices on the network * Add defaults when missing from config Implement monitor wait period from config * Add steady state monitor Remove duplicate callback registrations * Load devices into network orchestrator during testrun start --------- Co-authored-by: jhughesbiot * Build dependencies first (#21) * Build dependencies first * Remove debug message * Add depend on option to test modules * Re-add single interface option * Import subprocess --------- Co-authored-by: jhughesbiot * Port scan test module (#23) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add dns test module Fix test module build process * Add mac address of device under test to test container Update dns test to use mac address filter * Update dns module tests * Change result output * logging update * Update test module for better reusability * Load in module config to test module * logging cleanup * Update baseline module to new template Misc cleanup * Add ability to disable individual tests * remove duplicate readme * Update device directories * Remove local folder * Update device template Update test module to work with new device config file format * Change test module network config options Do not start network services for modules not configured for network * Initial nmap test module add Add device ip resolving to base module Add network mounting for test modules * Update ipv4 device resolving in test modules * Map in ip subnets and remove hard coded references * Add ftp port test * Add ability to pass config for individual tests within a module Update nmap module scan to run tests based on config * Add full module check for compliance * Add all tcp port scans to config * Update nmap commands to match existing DAQ tests Add udp scanning and tests * logging cleanup * Update TCP port scanning range Update logging * Merge device config into module config Update device template * fix merge issues * Update timeouts Add multi-threading for multiple scanns to run simultaneously Add option to use scan scripts for services * Fix merge issues * Fix device configs * Remove unecessary files * Cleanup duplicate properties * Cleanup install script * Formatting (#26) * Fix pylint issues in net orc * more pylint fixes * fix listener lint issues * fix logger lint issues * fix validator lint issues * fix util lint issues * Update base network module linting issues * Cleanup linter issues for dhcp modules Remove old code testing code * change to single quote delimeter * Cleanup linter issues for ntp module * Cleanup linter issues for radius module * Cleanup linter issues for template module * fix linter issues with faux-dev * Test results (#27) * Collect all module test results * Fix test modules without config options * Add timestamp to test results * Test results (#28) * Collect all module test results * Fix test modules without config options * Add timestamp to test results * Add attempt timing and device info to test results * Ignore disabled test containers when generating results * Fully skip modules that are disabled * Fix pylint test and skip internet tests so CI passes (#29) * disable internet checks for pass * fix pylint test * Increase pylint score (#31) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger --------- Co-authored-by: jhughesbiot * Pylint (#32) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting --------- Co-authored-by: Jacob Boddey * Add license header (#36) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module * add license header to all python files --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * Ovs (#35) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * remove ovs files added back in during merge * Nmap (#38) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module * add license header to all python files * Update tcp scans to speed up full port range scan Add version checking Implement ssh version checking * Add unknown port checks Match unknown ports to existing services Add unknown ports without existing services to results file --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * Create startup capture (#37) * Connection (#40) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Conn mac oui (#42) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Add mac_oui test Add option to return test result and details of test for reporting * Con mac address (#43) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Add mac_oui test Add option to return test result and details of test for reporting * Add connection.mac_address test * Dns (#44) * Add MDNS test * Update existing mdns logging to be more consistent with other tests * Add startup and monitor captures * File permissions (#45) * Fix validator file permissions * Fix test module permissions * Fix device capture file permissions * Fix device results permissions * Add connection single ip test (#47) * Nmap results (#49) * Update processing of nmap results to use xml output and json conversions for stability * Update matching with regex to prevent wrong service matches and duplicate processing for partial matches * Update max port scan range * Framework restructure (#50) * Restructure framework and modules * Fix CI paths * Fix base module * Add build script * Remove build logs * Update base and template docker files to fit the new format Implement a template option on network modules Fix skipping of base image build * remove base image build in ci * Remove group from chown --------- Co-authored-by: jhughesbiot * Ip control (#51) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * Move config to /local (#52) * Move config to /local * Fix testing config * Fix ovs_control config location * Fix faux dev config location * Add documentation (#53) * Sync dev to main (#56) * Merge dev into main (Sprint 7 and 8) (#33) * Implement test orchestrator (#4) * Initial work on test-orchestrator * Ignore runtime folder * Update runtime directory for test modules * Fix logging Add initial framework for running tests * logging and misc cleanup * logging changes * Add a stop hook after all tests complete * Refactor test_orc code * Add arg passing Add option to use locally cloned via install or remote via main project network orchestrator * Fix baseline module Fix orchestrator exiting only after timeout * Add result file to baseline test module Change result format to match closer to design doc * Refactor pylint * Skip test module if it failed to start * Refactor * Check for valid log level --------- Co-authored-by: Jacob Boddey * Add issue report templates (#7) * Add issue templates * Update README.md * Discover devices on the network (#5) * Test run sync (#8) * Initial work on test-orchestrator * Ignore runtime folder * Update runtime directory for test modules * Fix logging Add initial framework for running tests * logging and misc cleanup * logging changes * Add a stop hook after all tests complete * Refactor test_orc code * Add arg passing Add option to use locally cloned via install or remote via main project network orchestrator * Fix baseline module Fix orchestrator exiting only after timeout * Add result file to baseline test module Change result format to match closer to design doc * Refactor pylint * Skip test module if it failed to start * Refactor * Check for valid log level * Add config file arg Misc changes to network start procedure * fix merge issues * Update runner and test orch procedure Add useful runtiem args * Restructure test run startup process Misc updates to work with net orch updates * Refactor --------- * Quick refactor (#9) * Fix duplicate sleep calls * Add net orc (#11) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add the DNS test module (#12) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add dns test module Fix test module build process * Add mac address of device under test to test container Update dns test to use mac address filter * Update dns module tests * Change result output * logging update * Update test module for better reusability * Load in module config to test module * logging cleanup * Update baseline module to new template Misc cleanup * Add ability to disable individual tests * remove duplicate readme * Update device directories * Remove local folder * Update device template Update test module to work with new device config file format * Change test module network config options Do not start network services for modules not configured for network * Refactor --------- * Add baseline and pylint tests (#25) * Discover devices on the network (#22) * Discover devices on the network * Add defaults when missing from config Implement monitor wait period from config * Add steady state monitor Remove duplicate callback registrations * Load devices into network orchestrator during testrun start --------- Co-authored-by: jhughesbiot * Build dependencies first (#21) * Build dependencies first * Remove debug message * Add depend on option to test modules * Re-add single interface option * Import subprocess --------- Co-authored-by: jhughesbiot * Port scan test module (#23) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add dns test module Fix test module build process * Add mac address of device under test to test container Update dns test to use mac address filter * Update dns module tests * Change result output * logging update * Update test module for better reusability * Load in module config to test module * logging cleanup * Update baseline module to new template Misc cleanup * Add ability to disable individual tests * remove duplicate readme * Update device directories * Remove local folder * Update device template Update test module to work with new device config file format * Change test module network config options Do not start network services for modules not configured for network * Initial nmap test module add Add device ip resolving to base module Add network mounting for test modules * Update ipv4 device resolving in test modules * Map in ip subnets and remove hard coded references * Add ftp port test * Add ability to pass config for individual tests within a module Update nmap module scan to run tests based on config * Add full module check for compliance * Add all tcp port scans to config * Update nmap commands to match existing DAQ tests Add udp scanning and tests * logging cleanup * Update TCP port scanning range Update logging * Merge device config into module config Update device template * fix merge issues * Update timeouts Add multi-threading for multiple scanns to run simultaneously Add option to use scan scripts for services * Fix merge issues * Fix device configs * Remove unecessary files * Cleanup duplicate properties * Cleanup install script * Formatting (#26) * Fix pylint issues in net orc * more pylint fixes * fix listener lint issues * fix logger lint issues * fix validator lint issues * fix util lint issues * Update base network module linting issues * Cleanup linter issues for dhcp modules Remove old code testing code * change to single quote delimeter * Cleanup linter issues for ntp module * Cleanup linter issues for radius module * Cleanup linter issues for template module * fix linter issues with faux-dev * Test results (#27) * Collect all module test results * Fix test modules without config options * Add timestamp to test results * Test results (#28) * Collect all module test results * Fix test modules without config options * Add timestamp to test results * Add attempt timing and device info to test results * Ignore disabled test containers when generating results * Fully skip modules that are disabled * Fix pylint test and skip internet tests so CI passes (#29) * disable internet checks for pass * fix pylint test * Increase pylint score (#31) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger --------- Co-authored-by: jhughesbiot * Pylint (#32) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting --------- Co-authored-by: Jacob Boddey * Add license header (#36) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module * add license header to all python files --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * Ovs (#35) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * remove ovs files added back in during merge * Nmap (#38) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module * add license header to all python files * Update tcp scans to speed up full port range scan Add version checking Implement ssh version checking * Add unknown port checks Match unknown ports to existing services Add unknown ports without existing services to results file --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * Create startup capture (#37) * Connection (#40) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Conn mac oui (#42) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Add mac_oui test Add option to return test result and details of test for reporting * Con mac address (#43) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Add mac_oui test Add option to return test result and details of test for reporting * Add connection.mac_address test * Dns (#44) * Add MDNS test * Update existing mdns logging to be more consistent with other tests * Add startup and monitor captures * File permissions (#45) * Fix validator file permissions * Fix test module permissions * Fix device capture file permissions * Fix device results permissions * Add connection single ip test (#47) * Nmap results (#49) * Update processing of nmap results to use xml output and json conversions for stability * Update matching with regex to prevent wrong service matches and duplicate processing for partial matches * Update max port scan range * Framework restructure (#50) * Restructure framework and modules * Fix CI paths * Fix base module * Add build script * Remove build logs * Update base and template docker files to fit the new format Implement a template option on network modules Fix skipping of base image build * remove base image build in ci * Remove group from chown --------- Co-authored-by: jhughesbiot * Ip control (#51) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * Move config to /local (#52) * Move config to /local * Fix testing config * Fix ovs_control config location * Fix faux dev config location * Add documentation (#53) --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Co-authored-by: jhughesbiot Co-authored-by: Noureddine Co-authored-by: SuperJonotron * Sprint 8 Hotfix (#54) * Fix connection results.json * Re add try/catch * Fix log level * Debug test module load order * Add depends on to nmap module * Remove logging change --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Co-authored-by: jhughesbiot Co-authored-by: Noureddine Co-authored-by: SuperJonotron * Fix missing results on udp tests when tcp ports are also defined (#59) * Add licence header (#61) * Resolve merge conflict * Add network docs (#63) * Add network docs * Rename to readme * Add link to template module * Dhcp (#64) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * initial work on adding grpc functions for dhcp tests * rework code to allow for better usage and unit testing * working poc for test containers and grpc client to dhcp-1 * Move grpc client code into base image * Move grpc proto builds outside of dockerfile into module startup script * Setup pythonpath var in test module base startup process misc cleanup * pylinting and logging updates * Add python path resolving to network modules Update grpc path to prevent conflicts misc pylinting * Change lease resolving method to fix pylint issue * cleanup unit tests * cleanup unit tests * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * fix line endings * misc cleanup * Dhcp (#67) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * initial work on adding grpc functions for dhcp tests * rework code to allow for better usage and unit testing * working poc for test containers and grpc client to dhcp-1 * Move grpc client code into base image * Move grpc proto builds outside of dockerfile into module startup script * Setup pythonpath var in test module base startup process misc cleanup * pylinting and logging updates * Add python path resolving to network modules Update grpc path to prevent conflicts misc pylinting * Change lease resolving method to fix pylint issue * cleanup unit tests * cleanup unit tests * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * fix line endings * misc cleanup * Move isc-dhcp-server and radvd to services Move DHCP server monitoring and booting to python script * Add grpc methods to interact with dhcp_server module Update dhcp_server to control radvd server directly from calls Fix radvd service status method * Add updates to dhcp2 module Update radvd service * Add license headers * Add connection.dhcp_address test (#68) * Add NTP tests (#60) * Add ntp support test * Add extra log message * Modify descriptions * Pylint * Pylint (#69) --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> * Add ipv6 tests (#65) * Add ipv6 tests * Check for ND_NS * Connection private address (#71) * Add ntp support test * Add extra log message * Modify descriptions * Pylint * formatting * Change isc-dhcp service setup Fix dhcpd logging Add start and stop methods to grpc dhcp client Add dhcp2 client Inttial private_addr test * Add max lease time Add unit tests * fix last commit * finish initial work on test * pylinting * Breakup test and allow better failure reporting * restore network after test * Wait for device to get a lease from original dhcp range after network restore * pylinting * Fix ipv6 tests --------- Co-authored-by: Jacob Boddey * fix windows line ending * Fix python import * move isc-dhcp service commands to their own class update logging pylinting * fix dhcp1 * Initial CI testing for tests (#72) * Fix radvd conf * Fix individual test disable * Add NTP Pass CI test (#76) * add shared address test (#75) * Fix single ip test (#58) * Fix single ip test from detecting faux-device during validation as a failure * remove dhcp server capture file from scan --------- Co-authored-by: J Boddey * Merge API into dev (#70) * Start API * Write interfaces * Get current configuration * Set versions * Add more API methods * Correct no-ui flag * Do not launch API on baseline test * Move loading devices back to Test Run core * Merge dev into api (#74) * Merge dev into main (Add license header) (#62) Add license header * Add network docs (#63) * Add network docs * Rename to readme * Add link to template module * Dhcp (#64) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * initial work on adding grpc functions for dhcp tests * rework code to allow for better usage and unit testing * working poc for test containers and grpc client to dhcp-1 * Move grpc client code into base image * Move grpc proto builds outside of dockerfile into module startup script * Setup pythonpath var in test module base startup process misc cleanup * pylinting and logging updates * Add python path resolving to network modules Update grpc path to prevent conflicts misc pylinting * Change lease resolving method to fix pylint issue * cleanup unit tests * cleanup unit tests * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * fix line endings * misc cleanup * Dhcp (#67) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * initial work on adding grpc functions for dhcp tests * rework code to allow for better usage and unit testing * working poc for test containers and grpc client to dhcp-1 * Move grpc client code into base image * Move grpc proto builds outside of dockerfile into module startup script * Setup pythonpath var in test module base startup process misc cleanup * pylinting and logging updates * Add python path resolving to network modules Update grpc path to prevent conflicts misc pylinting * Change lease resolving method to fix pylint issue * cleanup unit tests * cleanup unit tests * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * fix line endings * misc cleanup * Move isc-dhcp-server and radvd to services Move DHCP server monitoring and booting to python script * Add grpc methods to interact with dhcp_server module Update dhcp_server to control radvd server directly from calls Fix radvd service status method * Add updates to dhcp2 module Update radvd service * Add license headers * Add connection.dhcp_address test (#68) * Add NTP tests (#60) * Add ntp support test * Add extra log message * Modify descriptions * Pylint * Pylint (#69) --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> * Add ipv6 tests (#65) * Add ipv6 tests * Check for ND_NS * Merge dev into main (Sprint 9) (#66) * Implement test orchestrator (#4) * Initial work on test-orchestrator * Ignore runtime folder * Update runtime directory for test modules * Fix logging Add initial framework for running tests * logging and misc cleanup * logging changes * Add a stop hook after all tests complete * Refactor test_orc code * Add arg passing Add option to use locally cloned via install or remote via main project network orchestrator * Fix baseline module Fix orchestrator exiting only after timeout * Add result file to baseline test module Change result format to match closer to design doc * Refactor pylint * Skip test module if it failed to start * Refactor * Check for valid log level --------- Co-authored-by: Jacob Boddey * Add issue report templates (#7) * Add issue templates * Update README.md * Discover devices on the network (#5) * Test run sync (#8) * Initial work on test-orchestrator * Ignore runtime folder * Update runtime directory for test modules * Fix logging Add initial framework for running tests * logging and misc cleanup * logging changes * Add a stop hook after all tests complete * Refactor test_orc code * Add arg passing Add option to use locally cloned via install or remote via main project network orchestrator * Fix baseline module Fix orchestrator exiting only after timeout * Add result file to baseline test module Change result format to match closer to design doc * Refactor pylint * Skip test module if it failed to start * Refactor * Check for valid log level * Add config file arg Misc changes to network start procedure * fix merge issues * Update runner and test orch procedure Add useful runtiem args * Restructure test run startup process Misc updates to work with net orch updates * Refactor --------- * Quick refactor (#9) * Fix duplicate sleep calls * Add net orc (#11) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add the DNS test module (#12) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add dns test module Fix test module build process * Add mac address of device under test to test container Update dns test to use mac address filter * Update dns module tests * Change result output * logging update * Update test module for better reusability * Load in module config to test module * logging cleanup * Update baseline module to new template Misc cleanup * Add ability to disable individual tests * remove duplicate readme * Update device directories * Remove local folder * Update device template Update test module to work with new device config file format * Change test module network config options Do not start network services for modules not configured for network * Refactor --------- * Add baseline and pylint tests (#25) * Discover devices on the network (#22) * Discover devices on the network * Add defaults when missing from config Implement monitor wait period from config * Add steady state monitor Remove duplicate callback registrations * Load devices into network orchestrator during testrun start --------- Co-authored-by: jhughesbiot * Build dependencies first (#21) * Build dependencies first * Remove debug message * Add depend on option to test modules * Re-add single interface option * Import subprocess --------- Co-authored-by: jhughesbiot * Port scan test module (#23) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add dns test module Fix test module build process * Add mac address of device under test to test container Update dns test to use mac address filter * Update dns module tests * Change result output * logging update * Update test module for better reusability * Load in module config to test module * logging cleanup * Update baseline module to new template Misc cleanup * Add ability to disable individual tests * remove duplicate readme * Update device directories * Remove local folder * Update device template Update test module to work with new device config file format * Change test module network config options Do not start network services for modules not configured for network * Initial nmap test module add Add device ip resolving to base module Add network mounting for test modules * Update ipv4 device resolving in test modules * Map in ip subnets and remove hard coded references * Add ftp port test * Add ability to pass config for individual tests within a module Update nmap module scan to run tests based on config * Add full module check for compliance * Add all tcp port scans to config * Update nmap commands to match existing DAQ tests Add udp scanning and tests * logging cleanup * Update TCP port scanning range Update logging * Merge device config into module config Update device template * fix merge issues * Update timeouts Add multi-threading for multiple scanns to run simultaneously Add option to use scan scripts for services * Fix merge issues * Fix device configs * Remove unecessary files * Cleanup duplicate properties * Cleanup install script * Formatting (#26) * Fix pylint issues in net orc * more pylint fixes * fix listener lint issues * fix logger lint issues * fix validator lint issues * fix util lint issues * Update base network module linting issues * Cleanup linter issues for dhcp modules Remove old code testing code * change to single quote delimeter * Cleanup linter issues for ntp module * Cleanup linter issues for radius module * Cleanup linter issues for template module * fix linter issues with faux-dev * Test results (#27) * Collect all module test results * Fix test modules without config options * Add timestamp to test results * Test results (#28) * Collect all module test results * Fix test modules without config options * Add timestamp to test results * Add attempt timing and device info to test results * Ignore disabled test containers when generating results * Fully skip modules that are disabled * Fix pylint test and skip internet tests so CI passes (#29) * disable internet checks for pass * fix pylint test * Increase pylint score (#31) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger --------- Co-authored-by: jhughesbiot * Pylint (#32) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting --------- Co-authored-by: Jacob Boddey * Add license header (#36) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module * add license header to all python files --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * Ovs (#35) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * remove ovs files added back in during merge * Nmap (#38) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module * add license header to all python files * Update tcp scans to speed up full port range scan Add version checking Implement ssh version checking * Add unknown port checks Match unknown ports to existing services Add unknown ports without existing services to results file --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * Create startup capture (#37) * Connection (#40) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Conn mac oui (#42) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Add mac_oui test Add option to return test result and details of test for reporting * Con mac address (#43) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Add mac_oui test Add option to return test result and details of test for reporting * Add connection.mac_address test * Dns (#44) * Add MDNS test * Update existing mdns logging to be more consistent with other tests * Add startup and monitor captures * File permissions (#45) * Fix validator file permissions * Fix test module permissions * Fix device capture file permissions * Fix device results permissions * Add connection single ip test (#47) * Nmap results (#49) * Update processing of nmap results to use xml output and json conversions for stability * Update matching with regex to prevent wrong service matches and duplicate processing for partial matches * Update max port scan range * Framework restructure (#50) * Restructure framework and modules * Fix CI paths * Fix base module * Add build script * Remove build logs * Update base and template docker files to fit the new format Implement a template option on network modules Fix skipping of base image build * remove base image build in ci * Remove group from chown --------- Co-authored-by: jhughesbiot * Ip control (#51) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * Move config to /local (#52) * Move config to /local * Fix testing config * Fix ovs_control config location * Fix faux dev config location * Add documentation (#53) * Sync dev to main (#56) * Merge dev into main (Sprint 7 and 8) (#33) * Implement test orchestrator (#4) * Initial work on test-orchestrator * Ignore runtime folder * Update runtime directory for test modules * Fix logging Add initial framework for running tests * logging and misc cleanup * logging changes * Add a stop hook after all tests complete * Refactor test_orc code * Add arg passing Add option to use locally cloned via install or remote via main project network orchestrator * Fix baseline module Fix orchestrator exiting only after timeout * Add result file to baseline test module Change result format to match closer to design doc * Refactor pylint * Skip test module if it failed to start * Refactor * Check for valid log level --------- Co-authored-by: Jacob Boddey * Add issue report templates (#7) * Add issue templates * Update README.md * Discover devices on the network (#5) * Test run sync (#8) * Initial work on test-orchestrator * Ignore runtime folder * Update runtime directory for test modules * Fix logging Add initial framework for running tests * logging and misc cleanup * logging changes * Add a stop hook after all tests complete * Refactor test_orc code * Add arg passing Add option to use locally cloned via install or remote via main project network orchestrator * Fix baseline module Fix orchestrator exiting only after timeout * Add result file to baseline test module Change result format to match closer to design doc * Refactor pylint * Skip test module if it failed to start * Refactor * Check for valid log level * Add config file arg Misc changes to network start procedure * fix merge issues * Update runner and test orch procedure Add useful runtiem args * Restructure test run startup process Misc updates to work with net orch updates * Refactor --------- * Quick refactor (#9) * Fix duplicate sleep calls * Add net orc (#11) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add the DNS test module (#12) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add dns test module Fix test module build process * Add mac address of device under test to test container Update dns test to use mac address filter * Update dns module tests * Change result output * logging update * Update test module for better reusability * Load in module config to test module * logging cleanup * Update baseline module to new template Misc cleanup * Add ability to disable individual tests * remove duplicate readme * Update device directories * Remove local folder * Update device template Update test module to work with new device config file format * Change test module network config options Do not start network services for modules not configured for network * Refactor --------- * Add baseline and pylint tests (#25) * Discover devices on the network (#22) * Discover devices on the network * Add defaults when missing from config Implement monitor wait period from config * Add steady state monitor Remove duplicate callback registrations * Load devices into network orchestrator during testrun start --------- Co-authored-by: jhughesbiot * Build dependencies first (#21) * Build dependencies first * Remove debug message * Add depend on option to test modules * Re-add single interface option * Import subprocess --------- Co-authored-by: jhughesbiot * Port scan test module (#23) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add dns test module Fix test module build process * Add mac address of device under test to test container Update dns test to use mac address filter * Update dns module tests * Change result output * logging update * Update test module for better reusability * Load in module config to test module * logging cleanup * Update baseline module to new template Misc cleanup * Add ability to disable individual tests * remove duplicate readme * Update device directories * Remove local folder * Update device template Update test module to work with new device config file format * Change test module network config options Do not start network services for modules not configured for network * Initial nmap test module add Add device ip resolving to base module Add network mounting for test modules * Update ipv4 device resolving in test modules * Map in ip subnets and remove hard coded references * Add ftp port test * Add ability to pass config for individual tests within a module Update nmap module scan to run tests based on config * Add full module check for compliance * Add all tcp port scans to config * Update nmap commands to match existing DAQ tests Add udp scanning and tests * logging cleanup * Update TCP port scanning range Update logging * Merge device config into module config Update device template * fix merge issues * Update timeouts Add multi-threading for multiple scanns to run simultaneously Add option to use scan scripts for services * Fix merge issues * Fix device configs * Remove unecessary files * Cleanup duplicate properties * Cleanup install script * Formatting (#26) * Fix pylint issues in net orc * more pylint fixes * fix listener lint issues * fix logger lint issues * fix validator lint issues * fix util lint issues * Update base network module linting issues * Cleanup linter issues for dhcp modules Remove old code testing code * change to single quote delimeter * Cleanup linter issues for ntp module * Cleanup linter issues for radius module * Cleanup linter issues for template module * fix linter issues with faux-dev * Test results (#27) * Collect all module test results * Fix test modules without config options * Add timestamp to test results * Test results (#28) * Collect all module test results * Fix test modules without config options * Add timestamp to test results * Add attempt timing and device info to test results * Ignore disabled test containers when generating results * Fully skip modules that are disabled * Fix pylint test and skip internet tests so CI passes (#29) * disable internet checks for pass * fix pylint test * Increase pylint score (#31) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger --------- Co-authored-by: jhughesbiot * Pylint (#32) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting --------- Co-authored-by: Jacob Boddey * Add license header (#36) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module * add license header to all python files --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * Ovs (#35) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * remove ovs files added back in during merge * Nmap (#38) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module * add license header to all python files * Update tcp scans to speed up full port range scan Add version checking Implement ssh version checking * Add unknown port checks Match unknown ports to existing services Add unknown ports without existing services to results file --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * Create startup capture (#37) * Connection (#40) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Conn mac oui (#42) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Add mac_oui test Add option to return test result and details of test for reporting * Con mac address (#43) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Add mac_oui test Add option to return test result and details of test for reporting * Add connection.mac_address test * Dns (#44) * Add MDNS test * Update existing mdns logging to be more consistent with other tests * Add startup and monitor captures * File permissions (#45) * Fix validator file permissions * Fix test module permissions * Fix device capture file permissions * Fix device results permissions * Add connection single ip test (#47) * Nmap results (#49) * Update processing of nmap results to use xml output and json conversions for stability * Update matching with regex to prevent wrong service matches and duplicate processing for partial matches * Update max port scan range * Framework restructure (#50) * Restructure framework and modules * Fix CI paths * Fix base module * Add build script * Remove build logs * Update base and template docker files to fit the new format Implement a template option on network modules Fix skipping of base image build * remove base image build in ci * Remove group from chown --------- Co-authored-by: jhughesbiot * Ip control (#51) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * Move config to /local (#52) * Move config to /local * Fix testing config * Fix ovs_control config location * Fix faux dev config location * Add documentation (#53) --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Co-authored-by: jhughesbiot Co-authored-by: Noureddine Co-authored-by: SuperJonotron * Sprint 8 Hotfix (#54) * Fix connection results.json * Re add try/catch * Fix log level * Debug test module load order * Add depends on to nmap module * Remove logging change --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Co-authored-by: jhughesbiot Co-authored-by: Noureddine Co-authored-by: SuperJonotron * Fix missing results on udp tests when tcp ports are also defined (#59) * Add licence header (#61) * Resolve merge conflict * Add network docs (#63) * Add network docs * Rename to readme * Add link to template module * Dhcp (#64) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * initial work on adding grpc functions for dhcp tests * rework code to allow for better usage and unit testing * working poc for test containers and grpc client to dhcp-1 * Move grpc client code into base image * Move grpc proto builds outside of dockerfile into module startup script * Setup pythonpath var in test module base startup process misc cleanup * pylinting and logging updates * Add python path resolving to network modules Update grpc path to prevent conflicts misc pylinting * Change lease resolving method to fix pylint issue * cleanup unit tests * cleanup unit tests * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * fix line endings * misc cleanup * Dhcp (#67) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * initial work on adding grpc functions for dhcp tests * rework code to allow for better usage and unit testing * working poc for test containers and grpc client to dhcp-1 * Move grpc client code into base image * Move grpc proto builds outside of dockerfile into module startup script * Setup pythonpath var in test module base startup process misc cleanup * pylinting and logging updates * Add python path resolving to network modules Update grpc path to prevent conflicts misc pylinting * Change lease resolving method to fix pylint issue * cleanup unit tests * cleanup unit tests * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * fix line endings * misc cleanup * Move isc-dhcp-server and radvd to services Move DHCP server monitoring and booting to python script * Add grpc methods to interact with dhcp_server module Update dhcp_server to control radvd server directly from calls Fix radvd service status method * Add updates to dhcp2 module Update radvd service * Add license headers * Add connection.dhcp_address test (#68) * Add NTP tests (#60) * Add ntp support test * Add extra log message * Modify descriptions * Pylint * Pylint (#69) --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> * Add ipv6 tests (#65) * Add ipv6 tests * Check for ND_NS --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Co-authored-by: jhughesbiot Co-authored-by: Noureddine Co-authored-by: SuperJonotron * Connection private address (#71) * Add ntp support test * Add extra log message * Modify descriptions * Pylint * formatting * Change isc-dhcp service setup Fix dhcpd logging Add start and stop methods to grpc dhcp client Add dhcp2 client Inttial private_addr test * Add max lease time Add unit tests * fix last commit * finish initial work on test * pylinting * Breakup test and allow better failure reporting * restore network after test * Wait for device to get a lease from original dhcp range after network restore * pylinting * Fix ipv6 tests --------- Co-authored-by: Jacob Boddey * fix windows line ending * Fix python import * move isc-dhcp service commands to their own class update logging pylinting * fix dhcp1 * Initial CI testing for tests (#72) * Fix radvd conf --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Co-authored-by: jhughesbiot Co-authored-by: Noureddine Co-authored-by: SuperJonotron * Fix testing command * Disable API on testing * Add API session * Remove old method * Remove local vars * Replace old var * Add device config * Add device configs * Fix paths * Change MAC address * Revert mac * Fix copy path * Debug loading devices * Remove reference * Changes * Re-add checks to prevent null values * Fix variable * Fix * Use dict instead of string * Try without json conversion * Container output to log * Undo changes to nmap module * Add post devices route --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Co-authored-by: jhughesbiot Co-authored-by: Noureddine Co-authored-by: SuperJonotron * Dhcp tests (#81) * Separate dhcp control methods into their own module Implement ip change test Add place holder for dhcp failover test * Stabilize network before leaving ip_change test Add dhcp_failover test * fix regression issue with individual test enable/disable setting * fix gitignore * Merge tls tests into dev (#80) * initial add of security module and tls tests * Fix server test and implement 1.3 version * pylinting * More work on client tests * Add client tls tests Add unit tets Add common python code to base test module * re-enable dhcp unit tests disabled during dev * rename module to tls * fix renaming * Fix unit tests broken by module rename Add TLS 1.3 tests to config * Add TLS 1.3 tests to config fix unit tests * Add certificate signature checks * Add local cert mounting for signature validatoin Fix test results * Update tls 1.2 server to pass with tls 1.3 compliance Add unit tests around tls 1.2 server Misc updates and cleanup * pylinting * Update cipher checks and add test * Fix test results when None is returned with details * Fix duplicate results --------- Co-authored-by: jhughesbiot * Test output restructure (#79) * Change runtime test structure to allow for multiple old tests * fix current test move * logging changes * Add device test count to device config * Change max report naming Add optional default value to system.json * Copy current test instead of moving to keep a consistent location of the most recent test * fix merge issue * pylint * Use local device folder and use session for config --------- Co-authored-by: Jacob Boddey * Keep test results in memory (#82) * Keep results in memory * More useful debug message * Fix file path * Result descriptions (#92) * Add short descriptions to conn module Add result_description to results with shorter wording for UI usage * Add result details to ipv6 tests * Update test descriptions in baseline module * Update dns test result details * Update skip results to include details when present * dns module formatting * add result details to nmap tests * add result details to ntp tests * Add short descriptions to tls module and formatting * misc test module formatting * fix typo * Misc cleanup (#93) * Fix network request from module config Misc formatting issues in test orchestrator * fix misc network orchestrator formatting issues * fix misc ovs control formatting issues * fix misc ip control formatting issues * Allow CORS (#91) * Allow CORS * Fix add device * Configurable API port * Add /history and device config endpoints (#88) * Add /history and device config endpoints * Add total tests * Add report to device * Only run tests if baseline passes * Re-enable actions, fix conn module (#89) * Re-enable actions, fix conn module * Fix net_orc init * Update report file name in testing * Add required result to module configs (#95) * Fix DNS test name (#110) * Clear runtime folder on start (#111) * Initial work on pdf report output and format via html (#103) * Add user interface (#98) * Add UI, small changes * Update UI * Update expected tests * Add some documentation * Save test modules (#115) * Expand testing to include API and more testing (#96) * Create Testrun package (#114) * Merge dev into main (Sprint 10 and 11) (#86) * Implement test orchestrator (#4) * Initial work on test-orchestrator * Ignore runtime folder * Update runtime directory for test modules * Fix logging Add initial framework for running tests * logging and misc cleanup * logging changes * Add a stop hook after all tests complete * Refactor test_orc code * Add arg passing Add option to use locally cloned via install or remote via main project network orchestrator * Fix baseline module Fix orchestrator exiting only after timeout * Add result file to baseline test module Change result format to match closer to design doc * Refactor pylint * Skip test module if it failed to start * Refactor * Check for valid log level --------- Co-authored-by: Jacob Boddey * Add issue report templates (#7) * Add issue templates * Update README.md * Discover devices on the network (#5) * Test run sync (#8) * Initial work on test-orchestrator * Ignore runtime folder * Update runtime directory for test modules * Fix logging Add initial framework for running tests * logging and misc cleanup * logging changes * Add a stop hook after all tests complete * Refactor test_orc code * Add arg passing Add option to use locally cloned via install or remote via main project network orchestrator * Fix baseline module Fix orchestrator exiting only after timeout * Add result file to baseline test module Change result format to match closer to design doc * Refactor pylint * Skip test module if it failed to start * Refactor * Check for valid log level * Add config file arg Misc changes to network start procedure * fix merge issues * Update runner and test orch procedure Add useful runtiem args * Restructure test run startup process Misc updates to work with net orch updates * Refactor --------- * Quick refactor (#9) * Fix duplicate sleep calls * Add net orc (#11) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add the DNS test module (#12) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add dns test module Fix test module build process * Add mac address of device under test to test container Update dns test to use mac address filter * Update dns module tests * Change result output * logging update * Update test module for better reusability * Load in module config to test module * logging cleanup * Update baseline module to new template Misc cleanup * Add ability to disable individual tests * remove duplicate readme * Update device directories * Remove local folder * Update device template Update test module to work with new device config file format * Change test module network config options Do not start network services for modules not configured for network * Refactor --------- * Add baseline and pylint tests (#25) * Discover devices on the network (#22) * Discover devices on the network * Add defaults when missing from config Implement monitor wait period from config * Add steady state monitor Remove duplicate callback registrations * Load devices into network orchestrator during testrun start --------- Co-authored-by: jhughesbiot * Build dependencies first (#21) * Build dependencies first * Remove debug message * Add depend on option to test modules * Re-add single interface option * Import subprocess --------- Co-authored-by: jhughesbiot * Port scan test module (#23) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add dns test module Fix test module build process * Add mac address of device under test to test container Update dns test to use mac address filter * Update dns module tests * Change result output * logging update * Update test module for better reusability * Load in module config to test module * logging cleanup * Update baseline module to new template Misc cleanup * Add ability to disable individual tests * remove duplicate readme * Update device directories * Remove local folder * Update device template Update test module to work with new device config file format * Change test module network config options Do not start network services for modules not configured for network * Initial nmap test module add Add device ip resolving to base module Add network mounting for test modules * Update ipv4 device resolving in test modules * Map in ip subnets and remove hard coded references * Add ftp port test * Add ability to pass config for individual tests within a module Update nmap module scan to run tests based on config * Add full module check for compliance * Add all tcp port scans to config * Update nmap commands to match existing DAQ tests Add udp scanning and tests * logging cleanup * Update TCP port scanning range Update logging * Merge device config into module config Update device template * fix merge issues * Update timeouts Add multi-threading for multiple scanns to run simultaneously Add option to use scan scripts for services * Fix merge issues * Fix device configs * Remove unecessary files * Cleanup duplicate properties * Cleanup install script * Formatting (#26) * Fix pylint issues in net orc * more pylint fixes * fix listener lint issues * fix logger lint issues * fix validator lint issues * fix util lint issues * Update base network module linting issues * Cleanup linter issues for dhcp modules Remove old code testing code * change to single quote delimeter * Cleanup linter issues for ntp module * Cleanup linter issues for radius module * Cleanup linter issues for template module * fix linter issues with faux-dev * Test results (#27) * Collect all module test results * Fix test modules without config options * Add timestamp to test results * Test results (#28) * Collect all module test results * Fix test modules without config options * Add timestamp to test results * Add attempt timing and device info to test results * Ignore disabled test containers when generating results * Fully skip modules that are disabled * Fix pylint test and skip internet tests so CI passes (#29) * disable internet checks for pass * fix pylint test * Increase pylint score (#31) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger --------- Co-authored-by: jhughesbiot * Pylint (#32) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting --------- Co-authored-by: Jacob Boddey * Add license header (#36) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module * add license header to all python files --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * Ovs (#35) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * remove ovs files added back in during merge * Nmap (#38) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module * add license header to all python files * Update tcp scans to speed up full port range scan Add version checking Implement ssh version checking * Add unknown port checks Match unknown ports to existing services Add unknown ports without existing services to results file --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * Create startup capture (#37) * Connection (#40) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Conn mac oui (#42) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Add mac_oui test Add option to return test result and details of test for reporting * Con mac address (#43) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Add mac_oui test Add option to return test result and details of test for reporting * Add connection.mac_address test * Dns (#44) * Add MDNS test * Update existing mdns logging to be more consistent with other tests * Add startup and monitor captures * File permissions (#45) * Fix validator file permissions * Fix test module permissions * Fix device capture file permissions * Fix device results permissions * Add connection single ip test (#47) * Nmap results (#49) * Update processing of nmap results to use xml output and json conversions for stability * Update matching with regex to prevent wrong service matches and duplicate processing for partial matches * Update max port scan range * Framework restructure (#50) * Restructure framework and modules * Fix CI paths * Fix base module * Add build script * Remove build logs * Update base and template docker files to fit the new format Implement a template option on network modules Fix skipping of base image build * remove base image build in ci * Remove group from chown --------- Co-authored-by: jhughesbiot * Ip control (#51) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * Move config to /local (#52) * Move config to /local * Fix testing config * Fix ovs_control config location * Fix faux dev config location * Add documentation (#53) * Sync dev to main (#56) * Merge dev into main (Sprint 7 and 8) (#33) * Implement test orchestrator (#4) * Initial work on test-orchestrator * Ignore runtime folder * Update runtime directory for test modules * Fix logging Add initial framework for running tests * logging and misc cleanup * logging changes * Add a stop hook after all tests complete * Refactor test_orc code * Add arg passing Add option to use locally cloned via install or remote via main project network orchestrator * Fix baseline module Fix orchestrator exiting only after timeout * Add result file to baseline test module Change result format to match closer to design doc * Refactor pylint * Skip test module if it failed to start * Refactor * Check for valid log level --------- Co-authored-by: Jacob Boddey * Add issue report templates (#7) * Add issue templates * Update README.md * Discover devices on the network (#5) * Test run sync (#8) * Initial work on test-orchestrator * Ignore runtime folder * Update runtime directory for test modules * Fix logging Add initial framework for running tests * logging and misc cleanup * logging changes * Add a stop hook after all tests complete * Refactor test_orc code * Add arg passing Add option to use locally cloned via install or remote via main project network orchestrator * Fix baseline module Fix orchestrator exiting only after timeout * Add result file to baseline test module Change result format to match closer to design doc * Refactor pylint * Skip test module if it failed to start * Refactor * Check for valid log level * Add config file arg Misc changes to network start procedure * fix merge issues * Update runner and test orch procedure Add useful runtiem args * Restructure test run startup process Misc updates to work with net orch updates * Refactor --------- * Quick refactor (#9) * Fix duplicate sleep calls * Add net orc (#11) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add the DNS test module (#12) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add dns test module Fix test module build process * Add mac address of device under test to test container Update dns test to use mac address filter * Update dns module tests * Change result output * logging update * Update test module for better reusability * Load in module config to test module * logging cleanup * Update baseline module to new template Misc cleanup * Add ability to disable individual tests * remove duplicate readme * Update device directories * Remove local folder * Update device template Update test module to work with new device config file format * Change test module network config options Do not start network services for modules not configured for network * Refactor --------- * Add baseline and pylint tests (#25) * Discover devices on the network (#22) * Discover devices on the network * Add defaults when missing from config Implement monitor wait period from config * Add steady state monitor Remove duplicate callback registrations * Load devices into network orchestrator during testrun start --------- Co-authored-by: jhughesbiot * Build dependencies first (#21) * Build dependencies first * Remove debug message * Add depend on option to test modules * Re-add single interface option * Import subprocess --------- Co-authored-by: jhughesbiot * Port scan test module (#23) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add dns test module Fix test module build process * Add mac address of device under test to test container Update dns test to use mac address filter * Update dns module tests * Change result output * logging update * Update test module for better reusability * Load in module config to test module * logging cleanup * Update baseline module to new template Misc cleanup * Add ability to disable individual tests * remove duplicate readme * Update device directories * Remove local folder * Update device template Update test module to work with new device config file format * Change test module network config options Do not start network services for modules not configured for network * Initial nmap test module add Add device ip resolving to base module Add network mounting for test modules * Update ipv4 device resolving in test modules * Map in ip subnets and remove hard coded references * Add ftp port test * Add ability to pass config for individual tests within a module Update nmap module scan to run tests based on config * Add full module check for compliance * Add all tcp port scans to config * Update nmap commands to match existing DAQ tests Add udp scanning and tests * logging cleanup * Update TCP port scanning range Update logging * Merge device config into module config Update device template * fix merge issues * Update timeouts Add multi-threading for multiple scanns to run simultaneously Add option to use scan scripts for services * Fix merge issues * Fix device configs * Remove unecessary files * Cleanup duplicate properties * Cleanup install script * Formatting (#26) * Fix pylint issues in net orc * more pylint fixes * fix listener lint issues * fix logger lint issues * fix validator lint issues * fix util lint issues * Update base network module linting issues * Cleanup linter issues for dhcp modules Remove old code testing code * change to single quote delimeter * Cleanup linter issues for ntp module * Cleanup linter issues for radius module * Cleanup linter issues for template module * fix linter issues with faux-dev * Test results (#27) * Collect all module test results * Fix test modules without config options * Add timestamp to test results * Test results (#28) * Collect all module test results * Fix test modules without config options * Add timestamp to test results * Add attempt timing and device info to test results * Ignore disabled test containers when generating results * Fully skip modules that are disabled * Fix pylint test and skip internet tests so CI passes (#29) * disable internet checks for pass * fix pylint test * Increase pylint score (#31) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger --------- Co-authored-by: jhughesbiot * Pylint (#32) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting --------- Co-authored-by: Jacob Boddey * Add license header (#36) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module * add license header to all python files --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * Ovs (#35) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * remove ovs files added back in during merge * Nmap (#38) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module * add license header to all python files * Update tcp scans to speed up full port range scan Add version checking Implement ssh version checking * Add unknown port checks Match unknown ports to existing services Add unknown ports without existing services to results file --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * Create startup capture (#37) * Connection (#40) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Conn mac oui (#42) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Add mac_oui test Add option to return test result and details of test for reporting * Con mac address (#43) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Add mac_oui test Add option to return test result and details of test for reporting * Add connection.mac_address test * Dns (#44) * Add MDNS test * Update existing mdns logging to be more consistent with other tests * Add startup and monitor captures * File permissions (#45) * Fix validator file permissions * Fix test module permissions * Fix device capture file permissions * Fix device results permissions * Add connection single ip test (#47) * Nmap results (#49) * Update processing of nmap results to use xml output and json conversions for stability * Update matching with regex to prevent wrong service matches and duplicate processing for partial matches * Update max port scan range * Framework restructure (#50) * Restructure framework and modules * Fix CI paths * Fix base module * Add build script * Remove build logs * Update base and template docker files to fit the new format Implement a template option on network modules Fix skipping of base image build * remove base image build in ci * Remove group from chown --------- Co-authored-by: jhughesbiot * Ip control (#51) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * Move config to /local (#52) * Move config to /local * Fix testing config * Fix ovs_control config location * Fix faux dev config location * Add documentation (#53) --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Co-authored-by: jhughesbiot Co-authored-by: Noureddine Co-authored-by: SuperJonotron * Sprint 8 Hotfix (#54) * Fix connection results.json * Re add try/catch * Fix log level * Debug test module load order * Add depends on to nmap module * Remove logging change --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Co-authored-by: jhughesbiot Co-authored-by: Noureddine Co-authored-by: SuperJonotron * Fix missing results on udp tests when tcp ports are also defined (#59) * Add licence header (#61) * Resolve merge conflict * Add network docs (#63) * Add network docs * Rename to readme * Add link to template module * Dhcp (#64) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * initial work on adding grpc functions for dhcp tests * rework code to allow for better usage and unit testing * working poc for test containers and grpc client to dhcp-1 * Move grpc client code into base image * Move grpc proto builds outside of dockerfile into module startup script * Setup pythonpath var in test module base startup process misc cleanup * pylinting and logging updates * Add python path resolving to network modules Update grpc path to prevent conflicts misc pylinting * Change lease resolving method to fix pylint issue * cleanup unit tests * cleanup unit tests * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * fix line endings * misc cleanup * Dhcp (#67) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * initial work on adding grpc functions for dhcp tests * rework code to allow for better usage and unit testing * working poc for test containers and grpc client to dhcp-1 * Move grpc client code into base image * Move grpc proto builds outside of dockerfile into module startup script * Setup pythonpath var in test module base startup process misc cleanup * pylinting and logging updates * Add python path resolving to network modules Update grpc path to prevent conflicts misc pylinting * Change lease resolving method to fix pylint issue * cleanup unit tests * cleanup unit tests * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * fix line endings * misc cleanup * Move isc-dhcp-server and radvd to services Move DHCP server monitoring and booting to python script * Add grpc methods to interact with dhcp_server module Update dhcp_server to control radvd server directly from calls Fix radvd service status method * Add updates to dhcp2 module Update radvd service * Add license headers * Add connection.dhcp_address test (#68) * Add NTP tests (#60) * Add ntp support test * Add extra log message * Modify descriptions * Pylint * Pylint (#69) --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> * Add ipv6 tests (#65) * Add ipv6 tests * Check for ND_NS * Connection private address (#71) * Add ntp support test * Add extra log message * Modify descriptions * Pylint * formatting * Change isc-dhcp service setup Fix dhcpd logging Add start and stop methods to grpc dhcp client Add dhcp2 client Inttial private_addr test * Add max lease time Add unit tests * fix last commit * finish initial work on test * pylinting * Breakup test and allow better failure reporting * restore network after test * Wait for device to get a lease from original dhcp range after network restore * pylinting * Fix ipv6 tests --------- Co-authored-by: Jacob Boddey * fix windows line ending * Fix python import * move isc-dhcp service commands to their own class update logging pylinting * fix dhcp1 * Initial CI testing for tests (#72) * Fix radvd conf * Fix individual test disable * Add NTP Pass CI test (#76) * add shared address test (#75) * Fix single ip test (#58) * Fix single ip test from detecting faux-device during validation as a failure * remove dhcp server capture file from scan --------- Co-authored-by: J Boddey * Merge API into dev (#70) * Start API * Write interfaces * Get current configuration * Set versions * Add more API methods * Correct no-ui flag * Do not launch API on baseline test * Move loading devices back to Test Run core * Merge dev into api (#74) * Merge dev into main (Add license header) (#62) Add license header * Add network docs (#63) * Add network docs * Rename to readme * Add link to template module * Dhcp (#64) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * initial work on adding grpc functions for dhcp tests * rework code to allow for better usage and unit testing * working poc for test containers and grpc client to dhcp-1 * Move grpc client code into base image * Move grpc proto builds outside of dockerfile into module startup script * Setup pythonpath var in test module base startup process misc cleanup * pylinting and logging updates * Add python path resolving to network modules Update grpc path to prevent conflicts misc pylinting * Change lease resolving method to fix pylint issue * cleanup unit tests * cleanup unit tests * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * fix line endings * misc cleanup * Dhcp (#67) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * initial work on adding grpc functions for dhcp tests * rework code to allow for better usage and unit testing * working poc for test containers and grpc client to dhcp-1 * Move grpc client code into base image * Move grpc proto builds outside of dockerfile into module startup script * Setup pythonpath var in test module base startup process misc cleanup * pylinting and logging updates * Add python path resolving to network modules Update grpc path to prevent conflicts misc pylinting * Change lease resolving method to fix pylint issue * cleanup unit tests * cleanup unit tests * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * fix line endings * misc cleanup * Move isc-dhcp-server and radvd to services Move DHCP server monitoring and booting to python script * Add grpc methods to interact with dhcp_server module Update dhcp_server to control radvd server directly from calls Fix radvd service status method * Add updates to dhcp2 module Update radvd service * Add license headers * Add connection.dhcp_address test (#68) * Add NTP tests (#60) * Add ntp support test * Add extra log message * Modify descriptions * Pylint * Pylint (#69) --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> * Add ipv6 tests (#65) * Add ipv6 tests * Check for ND_NS * Merge dev into main (Sprint 9) (#66) * Implement test orchestrator (#4) * Initial work on test-orchestrator * Ignore runtime folder * Update runtime directory for test modules * Fix logging Add initial framework for running tests * logging and misc cleanup * logging changes * Add a stop hook after all tests complete * Refactor test_orc code * Add arg passing Add option to use locally cloned via install or remote via main project network orchestrator * Fix baseline module Fix orchestrator exiting only after timeout * Add result file to baseline test module Change result format to match closer to design doc * Refactor pylint * Skip test module if it failed to start * Refactor * Check for valid log level --------- Co-authored-by: Jacob Boddey * Add issue report templates (#7) * Add issue templates * Update README.md * Discover devices on the network (#5) * Test run sync (#8) * Initial work on test-orchestrator * Ignore runtime folder * Update runtime directory for test modules * Fix logging Add initial framework for running tests * logging and misc cleanup * logging changes * Add a stop hook after all tests complete * Refactor test_orc code * Add arg passing Add option to use locally cloned via install or remote via main project network orchestrator * Fix baseline module Fix orchestrator exiting only after timeout * Add result file to baseline test module Change result format to match closer to design doc * Refactor pylint * Skip test module if it failed to start * Refactor * Check for valid log level * Add config file arg Misc changes to network start procedure * fix merge issues * Update runner and test orch procedure Add useful runtiem args * Restructure test run startup process Misc updates to work with net orch updates * Refactor --------- * Quick refactor (#9) * Fix duplicate sleep calls * Add net orc (#11) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add the DNS test module (#12) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add dns test module Fix test module build process * Add mac address of device under test to test container Update dns test to use mac address filter * Update dns module tests * Change result output * logging update * Update test module for better reusability * Load in module config to test module * logging cleanup * Update baseline module to new template Misc cleanup * Add ability to disable individual tests * remove duplicate readme * Update device directories * Remove local folder * Update device template Update test module to work with new device config file format * Change test module network config options Do not start network services for modules not configured for network * Refactor --------- * Add baseline and pylint tests (#25) * Discover devices on the network (#22) * Discover devices on the network * Add defaults when missing from config Implement monitor wait period from config * Add steady state monitor Remove duplicate callback registrations * Load devices into network orchestrator during testrun start --------- Co-authored-by: jhughesbiot * Build dependencies first (#21) * Build dependencies first * Remove debug message * Add depend on option to test modules * Re-add single interface option * Import subprocess --------- Co-authored-by: jhughesbiot * Port scan test module (#23) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add dns test module Fix test module build process * Add mac address of device under test to test container Update dns test to use mac address filter * Update dns module tests * Change result output * logging update * Update test module for better reusability * Load in module config to test module * logging cleanup * Update baseline module to new template Misc cleanup * Add ability to disable individual tests * remove duplicate readme * Update device directories * Remove local folder * Update device template Update test module to work with new device config file format * Change test module network config options Do not start network services for modules not configured for network * Initial nmap test module add Add device ip resolving to base module Add network mounting for test modules * Update ipv4 device resolving in test modules * Map in ip subnets and remove hard coded references * Add ftp port test * Add ability to pass config for individual tests within a module Update nmap module scan to run tests based on config * Add full module check for compliance * Add all tcp port scans to config * Update nmap commands to match existing DAQ tests Add udp scanning and tests * logging cleanup * Update TCP port scanning range Update logging * Merge device config into module config Update device template * fix merge issues * Update timeouts Add multi-threading for multiple scanns to run simultaneously Add option to use scan scripts for services * Fix merge issues * Fix device configs * Remove unecessary files * Cleanup duplicate properties * Cleanup install script * Formatting (#26) * Fix pylint issues in net orc * more pylint fixes * fix listener lint issues * fix logger lint issues * fix validator lint issues * fix util lint issues * Update base network module linting issues * Cleanup linter issues for dhcp modules Remove old code testing code * change to single quote delimeter * Cleanup linter issues for ntp module * Cleanup linter issues for radius module * Cleanup linter issues for template module * fix linter issues with faux-dev * Test results (#27) * Collect all module test results * Fix test modules without config options * Add timestamp to test results * Test results (#28) * Collect all module test results * Fix test modules without config options * Add timestamp to test results * Add attempt timing and device info to test results * Ignore disabled test containers when generating results * Fully skip modules that are disabled * Fix pylint test and skip internet tests so CI passes (#29) * disable internet checks for pass * fix pylint test * Increase pylint score (#31) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger --------- Co-authored-by: jhughesbiot * Pylint (#32) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting --------- Co-authored-by: Jacob Boddey * Add license header (#36) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module * add license header to all python files --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * Ovs (#35) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * remove ovs files added back in during merge * Nmap (#38) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module * add license header to all python files * Update tcp scans to speed up full port range scan Add version checking Implement ssh version checking * Add unknown port checks Match unknown ports to existing services Add unknown ports without existing services to results file --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * Create startup capture (#37) * Connection (#40) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Conn mac oui (#42) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Add mac_oui test Add option to return test result and details of test for reporting * Con mac address (#43) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Add mac_oui test Add option to return test result and details of test for reporting * Add connection.mac_address test * Dns (#44) * Add MDNS test * Update existing mdns logging to be more consistent with other tests * Add startup and monitor captures * File permissions (#45) * Fix validator file permissions * Fix test module permissions * Fix device capture file permissions * Fix device results permissions * Add connection single ip test (#47) * Nmap results (#49) * Update processing of nmap results to use xml output and json conversions for stability * Update matching with regex to prevent wrong service matches and duplicate processing for partial matches * Update max port scan range * Framework restructure (#50) * Restructure framework and modules * Fix CI paths * Fix base module * Add build script * Remove build logs * Update base and template docker files to fit the new format Implement a template option on network modules Fix skipping of base image build * remove base image build in ci * Remove group from chown --------- Co-authored-by: jhughesbiot * Ip control (#51) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * Move config to /local (#52) * Move config to /local * Fix testing config * Fix ovs_control config location * Fix faux dev config location * Add documentation (#53) * Sync dev to main (#56) * Merge dev into main (Sprint 7 and 8) (#33) * Implement test orchestrator (#4) * Initial work on test-orchestrator * Ignore runtime folder * Update runtime directory for test modules * Fix logging Add initial framework for running tests * logging and misc cleanup * logging changes * Add a stop hook after all tests complete * Refactor test_orc code * Add arg passing Add option to use locally cloned via install or remote via main project network orchestrator * Fix baseline module Fix orchestrator exiting only after timeout * Add result file to baseline test module Change result format to match closer to design doc * Refactor pylint * Skip test module if it failed to start * Refactor * Check for valid log level --------- Co-authored-by: Jacob Boddey * Add issue report templates (#7) * Add issue templates * Update README.md * Discover devices on the network (#5) * Test run sync (#8) * Initial work on test-orchestrator * Ignore runtime folder * Update runtime directory for test modules * Fix logging Add initial framework for running tests * logging and misc cleanup * logging changes * Add a stop hook after all tests complete * Refactor test_orc code * Add arg passing Add option to use locally cloned via install or remote via main project network orchestrator * Fix baseline module Fix orchestrator exiting only after timeout * Add result file to baseline test module Change result format to match closer to design doc * Refactor pylint * Skip test module if it failed to start * Refactor * Check for valid log level * Add config file arg Misc changes to network start procedure * fix merge issues * Update runner and test orch procedure Add useful runtiem args * Restructure test run startup process Misc updates to work with net orch updates * Refactor --------- * Quick refactor (#9) * Fix duplicate sleep calls * Add net orc (#11) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add the DNS test module (#12) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add dns test module Fix test module build process * Add mac address of device under test to test container Update dns test to use mac address filter * Update dns module tests * Change result output * logging update * Update test module for better reusability * Load in module config to test module * logging cleanup * Update baseline module to new template Misc cleanup * Add ability to disable individual tests * remove duplicate readme * Update device directories * Remove local folder * Update device template Update test module to work with new device config file format * Change test module network config options Do not start network services for modules not configured for network * Refactor --------- * Add baseline and pylint tests (#25) * Discover devices on the network (#22) * Discover devices on the network * Add defaults when missing from config Implement monitor wait period from config * Add steady state monitor Remove duplicate callback registrations * Load devices into network orchestrator during testrun start --------- Co-authored-by: jhughesbiot * Build dependencies first (#21) * Build dependencies first * Remove debug message * Add depend on option to test modules * Re-add single interface option * Import subprocess --------- Co-authored-by: jhughesbiot * Port scan test module (#23) * Add network orchestrator repository * cleanup duplicate start and install scripts * Temporary fix for python dependencies * Remove duplicate python requirements * remove duplicate conf files * remove remote-net option * cleanp unecessary files * Add dns test module Fix test module build process * Add mac address of device under test to test container Update dns test to use mac address filter * Update dns module tests * Change result output * logging update * Update test module for better reusability * Load in module config to test module * logging cleanup * Update baseline module to new template Misc cleanup * Add ability to disable individual tests * remove duplicate readme * Update device directories * Remove local folder * Update device template Update test module to work with new device config file format * Change test module network config options Do not start network services for modules not configured for network * Initial nmap test module add Add device ip resolving to base module Add network mounting for test modules * Update ipv4 device resolving in test modules * Map in ip subnets and remove hard coded references * Add ftp port test * Add ability to pass config for individual tests within a module Update nmap module scan to run tests based on config * Add full module check for compliance * Add all tcp port scans to config * Update nmap commands to match existing DAQ tests Add udp scanning and tests * logging cleanup * Update TCP port scanning range Update logging * Merge device config into module config Update device template * fix merge issues * Update timeouts Add multi-threading for multiple scanns to run simultaneously Add option to use scan scripts for services * Fix merge issues * Fix device configs * Remove unecessary files * Cleanup duplicate properties * Cleanup install script * Formatting (#26) * Fix pylint issues in net orc * more pylint fixes * fix listener lint issues * fix logger lint issues * fix validator lint issues * fix util lint issues * Update base network module linting issues * Cleanup linter issues for dhcp modules Remove old code testing code * change to single quote delimeter * Cleanup linter issues for ntp module * Cleanup linter issues for radius module * Cleanup linter issues for template module * fix linter issues with faux-dev * Test results (#27) * Collect all module test results * Fix test modules without config options * Add timestamp to test results * Test results (#28) * Collect all module test results * Fix test modules without config options * Add timestamp to test results * Add attempt timing and device info to test results * Ignore disabled test containers when generating results * Fully skip modules that are disabled * Fix pylint test and skip internet tests so CI passes (#29) * disable internet checks for pass * fix pylint test * Increase pylint score (#31) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger --------- Co-authored-by: jhughesbiot * Pylint (#32) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting --------- Co-authored-by: Jacob Boddey * Add license header (#36) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module * add license header to all python files --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * Ovs (#35) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * remove ovs files added back in during merge * Nmap (#38) * More formatting fixes * More formatting fixes * More formatting fixes * More formatting fixes * Misc pylint fixes Fix test module logger * remove unused files * more formatting * revert breaking pylint changes * more formatting * fix results file * More formatting * ovs module formatting * Add ovs control into network orchestrator * Add verification methods for the base network * Add network validation and misc logging updates * remove ovs module * add license header to all python files * Update tcp scans to speed up full port range scan Add version checking Implement ssh version checking * Add unknown port checks Match unknown ports to existing services Add unknown ports without existing services to results file --------- Co-authored-by: Jacob Boddey Co-authored-by: SuperJonotron * Create startup capture (#37) * Connection (#40) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Conn mac oui (#42) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Add mac_oui test Add option to return test result and details of test for reporting * Con mac address (#43) * Initial add of connection test module with ping test * Update host user resolving * Update host user resolving for validator * add get user method to validator * Add mac_oui test Add option to return test result and details of test for reporting * Add connection.mac_address test * Dns (#44) * Add MDNS test * Update existing mdns logging to be more consistent with other tests * Add startup and monitor captures * File permissions (#45) * Fix validator file permissions * Fix test module permissions * Fix device capture file permissions * Fix device results permissions * Add connection single ip test (#47) * Nmap results (#49) * Update processing of nmap results to use xml output and json conversions for stability * Update matching with regex to prevent wrong service matches and duplicate processing for partial matches * Update max port scan range * Framework restructure (#50) * Restructure framework and modules * Fix CI paths * Fix base module * Add build script * Remove build logs * Update base and template docker files to fit the new format Implement a template option on network modules Fix skipping of base image build * remove base image build in ci * Remove group from chown --------- Co-authored-by: jhughesbiot * Ip control (#51) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * Move config to /local (#52) * Move config to /local * Fix testing config * Fix ovs_control config location * Fix faux dev config location * Add documentation (#53) --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Co-authored-by: jhughesbiot Co-authored-by: Noureddine Co-authored-by: SuperJonotron * Sprint 8 Hotfix (#54) * Fix connection results.json * Re add try/catch * Fix log level * Debug test module load order * Add depends on to nmap module * Remove logging change --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Co-authored-by: jhughesbiot Co-authored-by: Noureddine Co-authored-by: SuperJonotron * Fix missing results on udp tests when tcp ports are also defined (#59) * Add licence header (#61) * Resolve merge conflict * Add network docs (#63) * Add network docs * Rename to readme * Add link to template module * Dhcp (#64) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * initial work on adding grpc functions for dhcp tests * rework code to allow for better usage and unit testing * working poc for test containers and grpc client to dhcp-1 * Move grpc client code into base image * Move grpc proto builds outside of dockerfile into module startup script * Setup pythonpath var in test module base startup process misc cleanup * pylinting and logging updates * Add python path resolving to network modules Update grpc path to prevent conflicts misc pylinting * Change lease resolving method to fix pylint issue * cleanup unit tests * cleanup unit tests * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * fix line endings * misc cleanup * Dhcp (#67) * Add initial work for ip control module * Implement ip control module with additional cleanup methods * Update link check to not use error stream * Add error checking around container network configurations * Add network cleanup for namespaces and links * formatting * initial work on adding grpc functions for dhcp tests * rework code to allow for better usage and unit testing * working poc for test containers and grpc client to dhcp-1 * Move grpc client code into base image * Move grpc proto builds outside of dockerfile into module startup script * Setup pythonpath var in test module base startup process misc cleanup * pylinting and logging updates * Add python path resolving to network modules Update grpc path to prevent conflicts misc pylinting * Change lease resolving method to fix pylint issue * cleanup unit tests * cleanup unit tests * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * Add grpc updates to dhcp2 module Update dhcp_config to deal with missing optional variables * fix line endings * misc cleanup * Move isc-dhcp-server and radvd to services Move DHCP server monitoring and booting to python script * Add grpc methods to interact with dhcp_server module Update dhcp_server to control radvd server directly from calls Fix radvd service status method * Add updates to dhcp2 module Update radvd service * Add license headers * Add connection.dhcp_address test (#68) * Add NTP tests (#60) * Add ntp support test * Add extra log message * Modify descriptions * Pylint * Pylint (#69) --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> * Add ipv6 tests (#65) * Add ipv6 tests * Check for ND_NS --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Co-authored-by: jhughesbiot Co-authored-by: Noureddine Co-authored-by: SuperJonotron * Connection private address (#71) * Add ntp support test * Add extra log message * Modify descriptions * Pylint * formatting * Change isc-dhcp service setup Fix dhcpd logging Add start and stop methods to grpc dhcp client Add dhcp2 client Inttial private_addr test * Add max lease time Add unit tests * fix last commit * finish initial work on test * pylinting * Breakup test and allow better failure reporting * restore network after test * Wait for device to get a lease from original dhcp range after network restore * pylinting * Fix ipv6 tests --------- Co-authored-by: Jacob Boddey * fix windows line ending * Fix python import * move isc-dhcp service commands to their own class update logging pylinting * fix dhcp1 * Initial CI testing for tests (#72) * Fix radvd conf --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Co-authored-by: jhughesbiot Co-authored-by: Noureddine Co-authored-by: SuperJonotron * Fix testing command * Disable API on testing * Add API session * Remove old method * Remove local vars * Replace old var * Add device config * Add device configs * Fix paths * Change MAC address * Revert mac * Fix copy path * Debug loading devices * Remove reference * Changes * Re-add checks to prevent null values * Fix variable * Fix * Use dict instead of string * Try without json conversion * Container output to log * Undo changes to nmap module * Add post devices route --------- Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Co-authored-by: jhughesbiot Co-authored-by: Noureddine Co-authored-by: SuperJonotron * Dhcp tests (#81) * Separate dhcp control methods into their own module Implement ip change test Add place holder for dhcp failover test * Stabilize network before leaving ip_change test Add dhcp_failover test * fix regression issue with individual test enable/disable setting * fix gitignore * Merge tls tests into dev (#80) * initial add of security module and tls tests * Fix server test and implement 1.3 version * pylinting * More work on client tests * Add client tls tests Add unit tets Add common python code to base test module … * Remove merge conflicts * Add extra validation (#118) * Refactor nmap module (#102) * Refactor nmap module * Fix version check * Add expected tests * Modify system config example * Fix nmap module * Fix nmap module * Add protocol module (#107) * Initial add of protocol module with bacnet tests * Add modbus test * Hotfix * bug fixes (#119) * bug fixes * Fix api tests * Change to in progress sooner * Disable module * Working actions * Update docs * Bug fixes * Add dependencies in make file * Update documentation * Chronyd (#116) * Change ntp server to use chronyd * Minor fixes to dhcp builds * Add conf line * Try CI network again * Update chrony.conf * Remove ntp from testing * Disable protocol module Signed-off-by: J Boddey --------- Signed-off-by: J Boddey Co-authored-by: Jacob Boddey * Test fixes (#123) * Mount root_certs to test containers * Add allow option to nmap ports Fix https failure detection in http test * Remove device certs copy during tls build * Update README.md Signed-off-by: J Boddey * Resolve 2 bugs (#121) * Fix 2x bugs * Modify testing * Bug/test count nmap check (#124) * Fix 2x bugs * Modify testing * Move test count --------- Signed-off-by: J Boddey * Report styling (#122) * Add formatting to report * Alter package * Fix report resource asset paths * Fix duplicate resource folder * cleanup * Update styling * Remove informational in this branch * Update styling * Remove Error status for now --------- Co-authored-by: Jacob Boddey * Update UI (#125) * Update UI * Update UI * Test fixes (#127) * Add docs, disable network validation by default (#126) * Add docs, disable network validation by default * Update docs --------- Signed-off-by: J Boddey Co-authored-by: jhughesbiot <50999916+jhughesbiot@users.noreply.github.com> Co-authored-by: jhughesbiot Co-authored-by: Noureddine Co-authored-by: SuperJonotron --- .github/workflows/testing.yml | 71 +- .gitignore | 1 + README.md | 40 +- bin/testrun | 13 +- cmd/build | 52 + cmd/install | 13 + cmd/package | 56 + cmd/prepare | 24 + docs/configure_device.md | 16 +- docs/dev/architecture.png | Bin 0 -> 133010 bytes docs/get_started.md | 79 +- docs/network/add_new_service.md | 4 +- docs/test/modules.md | 13 + docs/ui/device_icon.png | Bin 0 -> 933 bytes docs/ui/history_icon.png | Bin 0 -> 538 bytes docs/ui/progress_icon.png | Bin 0 -> 989 bytes docs/ui/settings_icon.png | Bin 0 -> 539 bytes docs/ui/settings_menu.png | Bin 0 -> 37230 bytes docs/ui/test_name.png | Bin 0 -> 10221 bytes framework/python/src/api/api.py | 48 +- framework/python/src/common/logger.py | 9 +- framework/python/src/common/session.py | 33 +- framework/python/src/common/testreport.py | 643 +- framework/python/src/common/util.py | 3 + framework/python/src/core/test_runner.py | 9 +- framework/python/src/core/testrun.py | 70 +- .../src/net_orc/network_orchestrator.py | 20 +- .../python/src/net_orc/network_validator.py | 2 +- framework/python/src/test_orc/module.py | 1 + .../python/src/test_orc/test_orchestrator.py | 104 +- framework/requirements.txt | 9 +- local/system.json.example | 4 +- make/.gitignore | 3 + make/DEBIAN/control | 8 + make/DEBIAN/postinst | 36 + modules/network/dhcp-1/dhcp-1.Dockerfile | 77 +- modules/network/dhcp-2/dhcp-2.Dockerfile | 77 +- modules/network/ntp/bin/start_network_service | 6 + modules/network/ntp/conf/chrony.conf | 62 + modules/network/ntp/ntp.Dockerfile | 66 +- modules/network/ntp/python/src/chronyd.py | 49 + modules/network/ntp/python/src/ntp_server.py | 354 +- modules/test/base/README.md | 19 + modules/test/base/python/src/test_module.py | 39 +- modules/test/baseline/README.md | 21 + modules/test/baseline/conf/module_config.json | 14 +- .../baseline/python/src/baseline_module.py | 18 +- modules/test/conn/README.md | 30 + modules/test/conn/conf/module_config.json | 54 +- .../test/conn/python/src/connection_module.py | 71 +- modules/test/dns/conf/module_config.json | 6 +- modules/test/dns/python/src/dns_module.py | 9 +- modules/test/nmap/conf/module_config.json | 485 +- modules/test/nmap/python/src/nmap_module.py | 516 +- modules/test/ntp/conf/module_config.json | 4 +- modules/test/protocol/bin/start_test_module | 53 + modules/test/protocol/conf/module_config.json | 56 + modules/test/protocol/protocol.Dockerfile | 34 + modules/test/protocol/python/requirements.txt | 7 + .../protocol/python/src/protocol_bacnet.py | 71 + .../protocol/python/src/protocol_modbus.py | 272 + .../protocol/python/src/protocol_module.py | 63 + modules/test/protocol/python/src/run.py | 68 + modules/test/tls/conf/module_config.json | 8 +- modules/test/tls/python/src/tls_module.py | 10 +- modules/test/tls/python/src/tls_util.py | 28 +- modules/test/tls/tls.Dockerfile | 4 - modules/ui/.editorconfig | 16 + modules/ui/.gitignore | 46 + modules/ui/angular.json | 104 + modules/ui/package-lock.json | 12407 ++++++++++++++++ modules/ui/package.json | 42 + modules/ui/src/app/app-routing.module.ts | 49 + modules/ui/src/app/app.component.html | 87 + modules/ui/src/app/app.component.scss | 119 + modules/ui/src/app/app.component.spec.ts | 215 + modules/ui/src/app/app.component.ts | 80 + modules/ui/src/app/app.module.ts | 55 + .../device-item/device-item.component.html | 23 + .../device-item/device-item.component.scss | 95 + .../device-item/device-item.component.spec.ts | 68 + .../device-item/device-item.component.ts | 32 + .../device-tests/device-tests.component.html | 25 + .../device-tests/device-tests.component.scss | 32 + .../device-tests.component.spec.ts | 94 + .../device-tests/device-tests.component.ts | 58 + .../download-report.component.html | 26 + .../download-report.component.scss | 22 + .../download-report.component.spec.ts | 113 + .../download-report.component.ts | 47 + .../general-settings.component.html | 75 + .../general-settings.component.scss | 120 + .../general-settings.component.spec.ts | 141 + .../general-settings.component.ts | 165 + .../only-different-values.validator.ts | 44 + .../device-form/device-form.component.html | 63 + .../device-form/device-form.component.scss | 61 + .../device-form/device-form.component.spec.ts | 390 + .../device-form/device-form.component.ts | 165 + .../device-form/device.validators.ts | 54 + .../device-repository-routing.module.ts | 27 + .../device-repository.component.html | 40 + .../device-repository.component.scss | 46 + .../device-repository.component.spec.ts | 189 + .../device-repository.component.ts | 68 + .../device-repository.module.ts | 56 + .../guards/allow-to-run-test.guard.spec.ts | 61 + .../src/app/guards/allow-to-run-test.guard.ts | 33 + .../src/app/history/history-routing.module.ts | 27 + .../ui/src/app/history/history.component.html | 82 + .../ui/src/app/history/history.component.scss | 98 + .../src/app/history/history.component.spec.ts | 152 + .../ui/src/app/history/history.component.ts | 64 + modules/ui/src/app/history/history.module.ts | 40 + modules/ui/src/app/mocks/device.mock.ts | 27 + modules/ui/src/app/mocks/progress.mock.ts | 69 + modules/ui/src/app/model/device.ts | 40 + modules/ui/src/app/model/setting.ts | 21 + modules/ui/src/app/model/testrun-status.ts | 75 + .../ui/src/app/notification.service.spec.ts | 61 + modules/ui/src/app/notification.service.ts | 32 + .../progress-breadcrumbs.component.html | 27 + .../progress-breadcrumbs.component.scss | 67 + .../progress-breadcrumbs.component.spec.ts | 38 + .../progress-breadcrumbs.component.ts | 27 + .../progress-initiate-form.component.html | 79 + .../progress-initiate-form.component.scss | 65 + .../progress-initiate-form.component.spec.ts | 300 + .../progress-initiate-form.component.ts | 143 + .../app/progress/progress-routing.module.ts | 27 + .../progress-status-card.component.html | 45 + .../progress-status-card.component.scss | 72 + .../progress-status-card.component.spec.ts | 274 + .../progress-status-card.component.ts | 69 + .../progress-table.component.html | 42 + .../progress-table.component.scss | 55 + .../progress-table.component.spec.ts | 109 + .../progress-table.component.ts | 37 + .../src/app/progress/progress.component.html | 78 + .../src/app/progress/progress.component.scss | 95 + .../app/progress/progress.component.spec.ts | 326 + .../ui/src/app/progress/progress.component.ts | 94 + .../ui/src/app/progress/progress.module.ts | 62 + modules/ui/src/app/test-run.service.spec.ts | 309 + modules/ui/src/app/test-run.service.ts | 193 + modules/ui/src/assets/.gitkeep | 0 modules/ui/src/assets/icons/close.svg | 3 + modules/ui/src/assets/icons/devices.svg | 5 + modules/ui/src/assets/icons/devices_add.svg | 9 + modules/ui/src/assets/icons/menu.svg | 10 + modules/ui/src/assets/icons/reports.svg | 5 + .../src/assets/icons/testrun_logo_color.svg | 40 + .../src/assets/icons/testrun_logo_small.svg | 40 + modules/ui/src/favicon.ico | Bin 0 -> 4286 bytes modules/ui/src/index.html | 37 + modules/ui/src/main.ts | 22 + modules/ui/src/styles.scss | 115 + modules/ui/src/theming/colors.scss | 157 + modules/ui/src/theming/theme.scss | 68 + modules/ui/src/theming/variables.scss | 18 + modules/ui/tsconfig.app.json | 14 + modules/ui/tsconfig.json | 33 + modules/ui/tsconfig.spec.json | 14 + modules/ui/ui.Dockerfile | 13 +- resources/devices/template/device_config.json | 6 + resources/report/Google Sans.woff2 | Bin 0 -> 21360 bytes resources/report/testrun.png | Bin 0 -> 4500 bytes testing/api/mockito/get_devices.json | 46 + testing/api/mockito/invalid_request.json | 3 + .../api/mockito/running_system_status.json | 26 + testing/api/system.json | 7 + testing/api/test_api | 52 + testing/api/test_api.py | 555 + testing/baseline/system.json | 7 + testing/baseline/test_baseline | 23 +- testing/baseline/test_baseline.py | 5 +- .../only_baseline/device_config.json | 28 + .../device_configs/tester1/device_config.json | 6 +- .../device_configs/tester2/device_config.json | 6 +- .../device_configs/tester3/device_config.json | 22 + testing/docker/ci_test_device1/Dockerfile | 2 +- testing/docker/ci_test_device1/entrypoint.sh | 42 +- testing/pylint/test_pylint | 2 +- testing/tests/system.json | 8 + testing/tests/test_tests | 76 +- testing/tests/test_tests.json | 41 +- testing/tests/test_tests.py | 24 +- ui/index.html | 1 - 188 files changed, 23361 insertions(+), 1339 deletions(-) create mode 100755 cmd/build create mode 100755 cmd/package create mode 100755 cmd/prepare create mode 100644 docs/dev/architecture.png create mode 100644 docs/test/modules.md create mode 100644 docs/ui/device_icon.png create mode 100644 docs/ui/history_icon.png create mode 100644 docs/ui/progress_icon.png create mode 100644 docs/ui/settings_icon.png create mode 100644 docs/ui/settings_menu.png create mode 100644 docs/ui/test_name.png create mode 100644 make/.gitignore create mode 100644 make/DEBIAN/control create mode 100755 make/DEBIAN/postinst create mode 100644 modules/network/ntp/conf/chrony.conf create mode 100644 modules/network/ntp/python/src/chronyd.py create mode 100644 modules/test/base/README.md create mode 100644 modules/test/baseline/README.md create mode 100644 modules/test/conn/README.md create mode 100644 modules/test/protocol/bin/start_test_module create mode 100644 modules/test/protocol/conf/module_config.json create mode 100644 modules/test/protocol/protocol.Dockerfile create mode 100644 modules/test/protocol/python/requirements.txt create mode 100644 modules/test/protocol/python/src/protocol_bacnet.py create mode 100644 modules/test/protocol/python/src/protocol_modbus.py create mode 100644 modules/test/protocol/python/src/protocol_module.py create mode 100644 modules/test/protocol/python/src/run.py create mode 100644 modules/ui/.editorconfig create mode 100644 modules/ui/.gitignore create mode 100644 modules/ui/angular.json create mode 100644 modules/ui/package-lock.json create mode 100644 modules/ui/package.json create mode 100644 modules/ui/src/app/app-routing.module.ts create mode 100644 modules/ui/src/app/app.component.html create mode 100644 modules/ui/src/app/app.component.scss create mode 100644 modules/ui/src/app/app.component.spec.ts create mode 100644 modules/ui/src/app/app.component.ts create mode 100644 modules/ui/src/app/app.module.ts create mode 100644 modules/ui/src/app/components/device-item/device-item.component.html create mode 100644 modules/ui/src/app/components/device-item/device-item.component.scss create mode 100644 modules/ui/src/app/components/device-item/device-item.component.spec.ts create mode 100644 modules/ui/src/app/components/device-item/device-item.component.ts create mode 100644 modules/ui/src/app/components/device-tests/device-tests.component.html create mode 100644 modules/ui/src/app/components/device-tests/device-tests.component.scss create mode 100644 modules/ui/src/app/components/device-tests/device-tests.component.spec.ts create mode 100644 modules/ui/src/app/components/device-tests/device-tests.component.ts create mode 100644 modules/ui/src/app/components/download-report/download-report.component.html create mode 100644 modules/ui/src/app/components/download-report/download-report.component.scss create mode 100644 modules/ui/src/app/components/download-report/download-report.component.spec.ts create mode 100644 modules/ui/src/app/components/download-report/download-report.component.ts create mode 100644 modules/ui/src/app/components/general-settings/general-settings.component.html create mode 100644 modules/ui/src/app/components/general-settings/general-settings.component.scss create mode 100644 modules/ui/src/app/components/general-settings/general-settings.component.spec.ts create mode 100644 modules/ui/src/app/components/general-settings/general-settings.component.ts create mode 100644 modules/ui/src/app/components/general-settings/only-different-values.validator.ts create mode 100644 modules/ui/src/app/device-repository/device-form/device-form.component.html create mode 100644 modules/ui/src/app/device-repository/device-form/device-form.component.scss create mode 100644 modules/ui/src/app/device-repository/device-form/device-form.component.spec.ts create mode 100644 modules/ui/src/app/device-repository/device-form/device-form.component.ts create mode 100644 modules/ui/src/app/device-repository/device-form/device.validators.ts create mode 100644 modules/ui/src/app/device-repository/device-repository-routing.module.ts create mode 100644 modules/ui/src/app/device-repository/device-repository.component.html create mode 100644 modules/ui/src/app/device-repository/device-repository.component.scss create mode 100644 modules/ui/src/app/device-repository/device-repository.component.spec.ts create mode 100644 modules/ui/src/app/device-repository/device-repository.component.ts create mode 100644 modules/ui/src/app/device-repository/device-repository.module.ts create mode 100644 modules/ui/src/app/guards/allow-to-run-test.guard.spec.ts create mode 100644 modules/ui/src/app/guards/allow-to-run-test.guard.ts create mode 100644 modules/ui/src/app/history/history-routing.module.ts create mode 100644 modules/ui/src/app/history/history.component.html create mode 100644 modules/ui/src/app/history/history.component.scss create mode 100644 modules/ui/src/app/history/history.component.spec.ts create mode 100644 modules/ui/src/app/history/history.component.ts create mode 100644 modules/ui/src/app/history/history.module.ts create mode 100644 modules/ui/src/app/mocks/device.mock.ts create mode 100644 modules/ui/src/app/mocks/progress.mock.ts create mode 100644 modules/ui/src/app/model/device.ts create mode 100644 modules/ui/src/app/model/setting.ts create mode 100644 modules/ui/src/app/model/testrun-status.ts create mode 100644 modules/ui/src/app/notification.service.spec.ts create mode 100644 modules/ui/src/app/notification.service.ts create mode 100644 modules/ui/src/app/progress/progress-breadcrumbs/progress-breadcrumbs.component.html create mode 100644 modules/ui/src/app/progress/progress-breadcrumbs/progress-breadcrumbs.component.scss create mode 100644 modules/ui/src/app/progress/progress-breadcrumbs/progress-breadcrumbs.component.spec.ts create mode 100644 modules/ui/src/app/progress/progress-breadcrumbs/progress-breadcrumbs.component.ts create mode 100644 modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.html create mode 100644 modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.scss create mode 100644 modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.spec.ts create mode 100644 modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.ts create mode 100644 modules/ui/src/app/progress/progress-routing.module.ts create mode 100644 modules/ui/src/app/progress/progress-status-card/progress-status-card.component.html create mode 100644 modules/ui/src/app/progress/progress-status-card/progress-status-card.component.scss create mode 100644 modules/ui/src/app/progress/progress-status-card/progress-status-card.component.spec.ts create mode 100644 modules/ui/src/app/progress/progress-status-card/progress-status-card.component.ts create mode 100644 modules/ui/src/app/progress/progress-table/progress-table.component.html create mode 100644 modules/ui/src/app/progress/progress-table/progress-table.component.scss create mode 100644 modules/ui/src/app/progress/progress-table/progress-table.component.spec.ts create mode 100644 modules/ui/src/app/progress/progress-table/progress-table.component.ts create mode 100644 modules/ui/src/app/progress/progress.component.html create mode 100644 modules/ui/src/app/progress/progress.component.scss create mode 100644 modules/ui/src/app/progress/progress.component.spec.ts create mode 100644 modules/ui/src/app/progress/progress.component.ts create mode 100644 modules/ui/src/app/progress/progress.module.ts create mode 100644 modules/ui/src/app/test-run.service.spec.ts create mode 100644 modules/ui/src/app/test-run.service.ts create mode 100644 modules/ui/src/assets/.gitkeep create mode 100644 modules/ui/src/assets/icons/close.svg create mode 100644 modules/ui/src/assets/icons/devices.svg create mode 100644 modules/ui/src/assets/icons/devices_add.svg create mode 100644 modules/ui/src/assets/icons/menu.svg create mode 100644 modules/ui/src/assets/icons/reports.svg create mode 100644 modules/ui/src/assets/icons/testrun_logo_color.svg create mode 100644 modules/ui/src/assets/icons/testrun_logo_small.svg create mode 100644 modules/ui/src/favicon.ico create mode 100644 modules/ui/src/index.html create mode 100644 modules/ui/src/main.ts create mode 100644 modules/ui/src/styles.scss create mode 100644 modules/ui/src/theming/colors.scss create mode 100644 modules/ui/src/theming/theme.scss create mode 100644 modules/ui/src/theming/variables.scss create mode 100644 modules/ui/tsconfig.app.json create mode 100644 modules/ui/tsconfig.json create mode 100644 modules/ui/tsconfig.spec.json create mode 100644 resources/report/Google Sans.woff2 create mode 100644 resources/report/testrun.png create mode 100644 testing/api/mockito/get_devices.json create mode 100644 testing/api/mockito/invalid_request.json create mode 100644 testing/api/mockito/running_system_status.json create mode 100644 testing/api/system.json create mode 100755 testing/api/test_api create mode 100644 testing/api/test_api.py create mode 100644 testing/baseline/system.json create mode 100644 testing/device_configs/only_baseline/device_config.json create mode 100644 testing/device_configs/tester3/device_config.json create mode 100644 testing/tests/system.json delete mode 100644 ui/index.html diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 87c8a814a..25b3a394a 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -1,7 +1,6 @@ name: Testrun test suite on: - push: pull_request: schedule: - cron: '0 13 * * *' @@ -14,7 +13,16 @@ jobs: steps: - name: Checkout source uses: actions/checkout@v2.3.4 - - name: Run tests + - name: Install dependencies + shell: bash {0} + run: cmd/prepare + - name: Package Testrun + shell: bash {0} + run: cmd/package + - name: Install Testrun + shell: bash {0} + run: sudo dpkg -i testrun*.deb + - name: Run baseline tests shell: bash {0} run: testing/baseline/test_baseline @@ -22,13 +30,52 @@ jobs: name: Tests runs-on: ubuntu-20.04 needs: testrun_baseline - timeout-minutes: 40 + timeout-minutes: 45 steps: - name: Checkout source uses: actions/checkout@v2.3.4 + - name: Install dependencies + shell: bash {0} + run: cmd/prepare + - name: Package Testrun + shell: bash {0} + run: cmd/package + - name: Install Testrun + shell: bash {0} + run: sudo dpkg -i testrun*.deb - name: Run tests shell: bash {0} run: testing/tests/test_tests + - name: Archive runtime results + if: ${{ always() }} + run: sudo tar --exclude-vcs -czf runtime.tgz /usr/local/testrun/runtime/ + - name: Upload runtime results + uses: actions/upload-artifact@v3 + if: ${{ always() }} + with: + if-no-files-found: error + name: runtime_${{ github.workflow }}_${{ github.run_id }} + path: runtime.tgz + + testrun_api: + name: API + runs-on: ubuntu-20.04 + timeout-minutes: 40 + steps: + - name: Checkout source + uses: actions/checkout@v2.3.4 + - name: Install dependencies + shell: bash {0} + run: cmd/prepare + - name: Package Testrun + shell: bash {0} + run: cmd/package + - name: Install Testrun + shell: bash {0} + run: sudo dpkg -i testrun*.deb + - name: Run tests + shell: bash {0} + run: testing/api/test_api pylint: name: Pylint @@ -37,6 +84,22 @@ jobs: steps: - name: Checkout source uses: actions/checkout@v2.3.4 - - name: Run tests + - name: Run pylint shell: bash {0} run: testing/pylint/test_pylint + + testrun_package: + name: Package + runs-on: ubuntu-22.04 + timeout-minutes: 5 + steps: + - name: Checkout source + uses: actions/checkout@v2.3.4 + - name: Package Testrun + shell: bash {0} + run: cmd/package + - name: Archive package + uses: actions/upload-artifact@v3 + with: + name: Testrun Installer + path: testrun*.deb \ No newline at end of file diff --git a/.gitignore b/.gitignore index 7ef392c5e..3f944ba34 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ pylint.out __pycache__/ build/ testing/unit_test/temp/ +*.deb \ No newline at end of file diff --git a/README.md b/README.md index 41c559499..404de4915 100644 --- a/README.md +++ b/README.md @@ -1,41 +1,42 @@ - Testrun logo + Testrun logo ## Introduction :wave: -Test Run is a tool to automate the validation of network-based functionality of IoT devices. Any device which is capable of receiving an IP address via DHCP is considered an IoT device by Test Run and can be tested. +Testrun automates specific test cases to verify network and security functionality in IoT devices. It is an open source tool which allows external manufacturers to test their devices for the purposes of Device Qualification within the BOS program. ## Motivation :bulb: -Without tools like Test Run, testing labs may be maintaining a large and complex network using equipment such as: A managed layer 3 switch, an enterprise-grade network router, virtualized or physical servers to provide DNS, NTP, 802.1x etc. With this amount of moving parts, all with dynamic configuration files and constant software updates, more time is likely to be spent on preparation and clean up of functinality or penetration testing - not forgetting the number of software tools required to perform the testing. The major issues which can and should be solved: +Without tools like Testrun, testing labs may be maintaining a large and complex network using equipment such as: A managed layer 3 switch, an enterprise-grade network router, virtualized or physical servers to provide DNS, NTP, 802.1x etc. With this amount of moving parts, all with dynamic configuration files and constant software updates, more time is likely to be spent on preparation and clean up of functinality or penetration testing - not forgetting the number of software tools required to perform the testing. The major issues which can and should be solved: 1) The complexity of managing a testing network 2) The time required to perform testing of network functionality 3) The accuracy and consistency of testing network functionality ## How it works :triangular_ruler: -Test Run creates an isolated and controlled network environment to fully simulate enterprise network deployments in your device testing lab. +Testrun creates an isolated and controlled network environment to fully simulate enterprise network deployments in your device testing lab. This removes the necessity for complex hardware, advanced knowledge and networking experience whilst enabling semi-technical engineers to validate device behaviour against industry cyber standards. -Two runtime modes will be supported by Test Run: +Two runtime modes are supported by Testrun: -1) Automated Testing +1) Automated testing Once the device has become operational (steady state), automated testing of the device under test will begin. Containerized test modules will then execute against the device (one module at a time). Once all test modules have completed execution, a final test report will be produced - presenting the results and further description of findings. 2) Lab network -Test Run cannot automate everything, and so additional manual testing may be required (or configuration changes may be required on the device). Rather than having to maintain a separate but idential lab network, Test Run will provide the network and some tools to assist an engineer performing the additional testing. At the same time, packet captures of the device behaviour will be recorded, alongside logs for each network service, for further debugging. +Testrun cannot automate everything, and so additional manual testing may be required (or configuration changes may be required on the device). Rather than having to maintain a separate but idential lab network, Testrun will provide the network and some tools to assist an engineer performing the additional testing. At the same time, packet captures of the device behaviour will be recorded, alongside logs for each network service, for further debugging. -## Minimum Requirements :computer: +## Minimum requirements :computer: ### Hardware - PC running Ubuntu LTS (laptop or desktop) - 2x USB ethernet adapter (One may be built in ethernet) - Internet connection ### Software - - Python 3 (Already available on Ubuntu LTS) - - Docker - [Install guide](https://docs.docker.com/engine/install/ubuntu/) - - Open vSwitch ``sudo apt-get install openvswitch-common openvswitch-switch`` +- Docker - installation guide: [https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) + +## Get started ▶️ +Once you have met the hardware and software requirements, you can get started with Testrun by following the [Get started guide](docs/get_started.md). ## Roadmap :chart_with_upwards_trend: -Test Run will constantly evolve to further support end-users by automating device network behaviour against industry standards. +Testrun will constantly evolve to further support end-users by automating device network behaviour against industry standards. ## Issue reporting :triangular_flag_on_post: If the application has come across a problem at any point during setup or use, please raise an issue under the [issues tab](https://github.com/auto-iot/test-run/issues). Issue templates exist for both bug reports and feature requests. If neither of these are appropriate for your issue, raise a blank issue instead. @@ -44,25 +45,26 @@ If the application has come across a problem at any point during setup or use, p The contributing requirements can be found in [CONTRIBUTING.md](CONTRIBUTING.md). In short, checkout the [Google CLA](https://cla.developers.google.com/) site to get started. ## FAQ :raising_hand: -1) What device networking functionality is validated by Test Run? +1) What device networking functionality is validated by Testrun? Best practices and requirements for IoT devices are constantly changing due to technological advances and discovery of vulnerabilities. The current expectations for IoT devices on Google deployments can be found in the [Application Security Requirements for IoT Devices](https://partner-security.withgoogle.com/docs/iot_requirements). - Test Run aims to automate as much of the Application Security Requirements as possible. + Testrun aims to automate as much of the Application Security Requirements as possible. 2) What services are provided on the virtual network? The following are network services that are containerized and accessible to the device under test though are likely to change over time: - DHCP in failover configuration with internet connectivity - - DNS (and DNS over HTTPS) + - IPv6 SLAAC + - DNS - NTPv4 - 802.1x Port Based Authentication -3) Can I run Test Run on a virtual machine? +3) Can I run Testrun on a virtual machine? - Probably. Provided that the required 2x USB ethernet adapters are passed to the virtual machine as USB devices rather than network adapters, Test Run should - still work. We will look to test and approve the use of virtualisation to run Test Run in the future. + Probably. Provided that the required 2x USB ethernet adapters are passed to the virtual machine as USB devices rather than network adapters, Testrun should + still work. We will look to test and approve the use of virtualisation to run Testrun in the future. - 4) Can I connect multiple devices to Test Run? + 4) Can I connect multiple devices to Testrun? In short, Yes you can. The way in which multiple devices could be tested simultaneously is yet to be decided. However, if you simply want to add field/peer devices during runtime (even another laptop performing manual testing) then you may connect the USB ethernet adapter to an unmanaged switch. diff --git a/bin/testrun b/bin/testrun index 9281c1ac6..41af70ffe 100755 --- a/bin/testrun +++ b/bin/testrun @@ -15,7 +15,7 @@ # limitations under the License. if [[ "$EUID" -ne 0 ]]; then - echo "Must run as root. Use sudo testrun" + echo "Must run as root. Use sudo $0" exit 1 fi @@ -26,17 +26,20 @@ fi # Ensure that /var/run/netns folder exists sudo mkdir -p /var/run/netns +export TESTRUNPATH=/usr/local/testrun +cd $TESTRUNPATH + # Create device folder if it doesn't exist mkdir -p local/devices -# Check if Python modules exist. Install if not -[ ! -d "venv" ] && sudo cmd/install +# Remove existing runtime data +rm -rf runtime/* # Activate Python virtual environment source venv/bin/activate # Set the PYTHONPATH to include the "src" directory -export PYTHONPATH="$PWD/framework/python/src" -python -u framework/python/src/core/test_runner.py $@ +export PYTHONPATH="$TESTRUNPATH/framework/python/src" +python -u framework/python/src/core/test_runner.py $@ 2>&1 | tee testrun.log deactivate \ No newline at end of file diff --git a/cmd/build b/cmd/build new file mode 100755 index 000000000..5143e8902 --- /dev/null +++ b/cmd/build @@ -0,0 +1,52 @@ +#!/bin/bash -e + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Builds all docker images +echo Building docker images + +# Build user interface +echo Building user interface +mkdir -p build/ui +docker build -t test-run/ui -f modules/ui/ui.Dockerfile . 2>&1 | tee build/ui/ui.log + +# Build network modules +echo Building network modules +mkdir -p build/network +for dir in modules/network/* ; do + module=$(basename $dir) + echo Building network module $module... + docker build -f modules/network/$module/$module.Dockerfile -t test-run/$module . 2>&1 | tee build/network/$module.log +done + +# Build validators +echo Building network validators +mkdir -p build/devices +for dir in modules/devices/* ; do + module=$(basename $dir) + echo Building validator module $module... + docker build -f modules/devices/$module/$module.Dockerfile -t test-run/$module . 2>&1 | tee build/devices/$module.log +done + +# Build test modules +echo Building test modules +mkdir -p build/test +for dir in modules/test/* ; do + module=$(basename $dir) + echo Building test module $module... + docker build -f modules/test/$module/$module.Dockerfile -t test-run/$module-test . 2>&1 | tee build/test/$module.log +done + +echo Finished building modules \ No newline at end of file diff --git a/cmd/install b/cmd/install index 4e8639a66..0b6ac92de 100755 --- a/cmd/install +++ b/cmd/install @@ -14,10 +14,23 @@ # See the License for the specific language governing permissions and # limitations under the License. +echo Installing application dependencies + +TESTRUN_DIR=/usr/local/testrun +cd $TESTRUN_DIR + python3 -m venv venv source venv/bin/activate pip3 install -r framework/requirements.txt +# Copy the default configuration +cp -n local/system.json.example local/system.json + deactivate + +# Build docker images +sudo cmd/build + +echo Finished installing Testrun diff --git a/cmd/package b/cmd/package new file mode 100755 index 000000000..5f24273ac --- /dev/null +++ b/cmd/package @@ -0,0 +1,56 @@ +#!/bin/bash -e + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Creates a package for Testrun + +MAKE_SRC_DIR=make + +# Copy testrun script to /bin +mkdir -p $MAKE_SRC_DIR/bin +cp bin/testrun $MAKE_SRC_DIR/bin/testrun + +# Create testrun folder +mkdir -p $MAKE_SRC_DIR/usr/local/testrun + +# Create postinst script +cp cmd/install $MAKE_SRC_DIR/DEBIAN/postinst + +# Copy other commands +mkdir -p $MAKE_SRC_DIR/usr/local/testrun/cmd +cp cmd/{prepare,build} $MAKE_SRC_DIR/usr/local/testrun/cmd + +# Copy resources +cp -r resources $MAKE_SRC_DIR/usr/local/testrun/ + +# Create local folder +mkdir -p $MAKE_SRC_DIR/usr/local/testrun/local +cp local/system.json.example $MAKE_SRC_DIR/usr/local/testrun/local/system.json.example + +# Create device repository +mkdir -p $MAKE_SRC_DIR/usr/local/testrun/local/devices + +# Copy root_certs folder +mkdir -p local/root_certs +cp -r local/root_certs $MAKE_SRC_DIR/usr/local/testrun/local/root_certs + +# Copy framework and modules into testrun folder +cp -r {framework,modules} $MAKE_SRC_DIR/usr/local/testrun + +# Build .deb file +dpkg-deb --build --root-owner-group make + +# Rename the .deb file +mv make.deb testrun_1-0_amd64.deb \ No newline at end of file diff --git a/cmd/prepare b/cmd/prepare new file mode 100755 index 000000000..950051bd3 --- /dev/null +++ b/cmd/prepare @@ -0,0 +1,24 @@ +#!/bin/bash -e + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Optional script to prepare your system for use with Testrun. +# Installs system dependencies + +echo Installing system dependencies + +sudo apt-get install openvswitch-common openvswitch-switch python3 libpangocairo-1.0-0 + +echo Finished installing system dependencies diff --git a/docs/configure_device.md b/docs/configure_device.md index ad58521a4..9eefcd866 100644 --- a/docs/configure_device.md +++ b/docs/configure_device.md @@ -8,24 +8,12 @@ The device information section includes the manufacturer, model, and MAC address ## Test Modules -Test modules are groups of tests that can be enabled or disabled as needed. You can choose which test modules to include for your device. The device configuration file contains the following test module: - -- DNS Test Module +Test modules are groups of tests that can be enabled or disabled as needed. You can choose which test modules to run on your device. ### Enabling and Disabling Test Modules To enable or disable a test module, modify the `enabled` field within the respective module. Setting it to `true` enables the module, while setting it to `false` disables the module. -## Individual Tests - -Within the DNS test module, there are individual tests that can be enabled or disabled. These tests focus on specific aspects of network behavior. You can customize the tests based on your device and testing requirements. - -### Enabling and Disabling Tests - -To enable or disable an individual test, modify the `enabled` field within the respective test. Setting it to `true` enables the test, while setting it to `false` disables the test. - -> Note: The example device configuration file (`resources/devices/template/device_config.json`) provides a complete usage example, including the structure and configuration options for the DNS test module and its tests. You can refer to this file to understand how to configure your device tests effectively. - ## Customizing the Device Configuration To customize the device configuration for your specific device, follow these steps: @@ -38,4 +26,4 @@ This ensures that you have a copy of the default configuration file, which you c > Note: Ensure that the device configuration file is properly formatted, and the changes made align with the intended test behavior. Incorrect settings or syntax may lead to unexpected results during testing. -If you encounter any issues or need assistance with the device configuration, refer to the Test Run documentation or ask a question on the Issues page. +If you encounter any issues or need assistance with the device configuration, refer to the Testrun documentation or ask a question on the Issues page. diff --git a/docs/dev/architecture.png b/docs/dev/architecture.png new file mode 100644 index 0000000000000000000000000000000000000000..e349141b66596362ae4626ae4b05d758b3ddc5e1 GIT binary patch literal 133010 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 u(5tgh0?R3vNm^8Wxl-gMRg literal 0 HcmV?d00001 diff --git a/docs/get_started.md b/docs/get_started.md index 7b8cf9e13..f201ee8b4 100644 --- a/docs/get_started.md +++ b/docs/get_started.md @@ -1,10 +1,12 @@ # Getting Started +It is recommended that you run Testrun on a standalone machine running a fresh-install of Ubuntu 22.04.3 LTS. + ## Prerequisites ### Hardware -Before starting with Test Run, ensure you have the following hardware: +Before starting with Testrun, ensure you have the following hardware: - PC running Ubuntu LTS (laptop or desktop) - 2x USB Ethernet adapter (one may be a built-in Ethernet port) @@ -13,35 +15,76 @@ Before starting with Test Run, ensure you have the following hardware: ### Software Ensure the following software is installed on your Ubuntu LTS PC: - -- Python 3 (already available on Ubuntu LTS) -- Docker - Installation Guide: [https://docs.docker.com/engine/install/](https://docs.docker.com/engine/install/) -- Open vSwitch ``sudo apt-get install openvswitch-common openvswitch-switch`` +- Docker - installation guide: [https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) +- System dependencies (These will be installed automatically when installing Testrun if not already installed): + - Python3-dev + - Python3-venv + - Openvswitch Common + - Openvswitch Switch + - Build Essential + - Net Tools ## Installation -1. Download Test Run from the releases page or the appropriate source. +1. Download the latest version of the Testrun installer from the [releases page](https://github.com/google/test-run/releases) + +2. Open a terminal and navigate to location of the Testrun installer (most likely your downloads folder) -2. Run the install script. +3. Install the package using ``sudo apt install ./testrun*.deb`` -## Configuration + - Testrun will be installed under the /usr/local/testrun directory. + - Testing data will be available in the ``local/devices/{device}/reports`` folders + - Additional configuration options are available in the ``local/system.json`` file + +## Start Testrun + +1. Attach network interfaces: + - Connect one USB Ethernet adapter to the internet source (e.g., router or switch) using an ethernet cable. + - Connect the other USB Ethernet adapter directly to the IoT device you want to test using an ethernet cable. -1. Copy the default configuration file. + **NOTE: Both adapters should be disabled in the host system (IPv4, IPv6 and general). You can do this by going to Settings > Network** -2. Open the `local/system.json` file and modify the configuration as needed. Specify the interface names for the internet and device interfaces. +2. Start Testrun. + +Start Testrun with the command `sudo testrun` + + - To run Testrun in network-only mode (without running any tests), use the `--net-only` option. + + - To run Testrun with just one interface (connected to the device), use the ``--single-intf`` option. ## Test Your Device -1. Attach network interfaces: +1. Once Testrun has started, open your browser to http://localhost:8080. + +2. Configure your network interfaces under the settings menu - located in the top right corner of the application. Settings can be changed at any time. + + ![](/docs/ui/settings_icon.png) + +3. Navigate to the device repository icon to add a new device for testing. + + ![](/docs/ui/device_icon.png) + +4. Click the button 'Add Device'. + +5. Enter the MAC address, manufacturer name and model number. + +6. Select the test modules you wish to enable for this device (Hint: All are required for qualification purposes) and click save. + +7. Navigate to the Testrun progress icon and click the button 'Start New Testrun'. + + ![](/docs/ui/progress_icon.png) + +8. Select the device you would like to test. + +9. Enter the version number of the firmware running on the device. - - Connect one USB Ethernet adapter to the internet source (e.g., router or switch) using an Ethernet cable. - - Connect the other USB Ethernet adapter directly to the IoT device you want to test using an Ethernet cable. +10. Click 'Start Testrun' -2. Start Test Run. + - During testing, if you would like to stop Testrun, click 'Stop' next to the test name. - - To run Test Run in network-only mode (without running any tests), use the `--net-only` option. +11. On completion of the test sequence, a report will appear under the history icon. - - To skip network validation before use and not launch the faux device on startup, use the `--no-validate` option. + ![](/docs/ui/history_icon.png) # Troubleshooting @@ -49,5 +92,5 @@ If you encounter any issues or need assistance, consider the following: - Ensure that all hardware and software prerequisites are met. - Verify that the network interfaces are connected correctly. -- Check the configuration in the `local/system.json` file. -- Refer to the Test Run documentation or ask for further assistance from the support team. +- Check the configuration settings. +- Refer to the Testrun documentation or ask for assistance in the issues page: https://github.com/google/testrun/issues diff --git a/docs/network/add_new_service.md b/docs/network/add_new_service.md index 1ad07b60d..5f7b470cd 100644 --- a/docs/network/add_new_service.md +++ b/docs/network/add_new_service.md @@ -1,8 +1,8 @@ # Adding a New Network Service -The Test Run framework allows users to add their own network services with ease. A template network service can be used to get started quickly, this can be found at [modules/network/template](../../modules/network/template). Otherwise, see below for details of the requirements for new network services. +The Testrun framework allows users to add their own network services with ease. A template network service can be used to get started quickly, this can be found at [modules/network/template](../../modules/network/template). Otherwise, see below for details of the requirements for new network services. -To add a new network service to Test Run, follow the procedure below: +To add a new network service to Testrun, follow the procedure below: 1. Create a folder under `modules/network/` with the name of the network service in lowercase, using only alphanumeric characters and hyphens (`-`). 2. Inside the created folder, include the following files and folders: diff --git a/docs/test/modules.md b/docs/test/modules.md new file mode 100644 index 000000000..a3016e17f --- /dev/null +++ b/docs/test/modules.md @@ -0,0 +1,13 @@ +# Test Modules + +Testrun provides some pre-built test modules for you to use when testing your own device. These test modules are listed below: + +| Name | Description | Read more | +|---|---|---| +| Base | Template for all test modules | [Base module](/modules/test/base/README.md) | +| Baseline | A sample test module | [Baseline module](/modules/test/baseline/README.md) | +| Connection | Verify IP and DHCP based behavior | [Connection module](/modules/test/conn/README.md) | +| DNS | Verify DNS functionality | [DNS module](/modules/test/dns/README.md) | +| NMAP | Ensure unsecure services are disabled | [NMAP module](/modules/test/nmap/README.md) | +| NTP | Verify NTP functionality | [NTP module](/modules/test/ntp/README.md) | +| TLS | Determine TLS client and server behavior | [TLS module](/modules/test/tls/README.md) | \ No newline at end of file diff --git a/docs/ui/device_icon.png b/docs/ui/device_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..2472f7da223c4b6a29e94e830303095628d3aae6 GIT binary patch literal 933 zcmV;W16urvP)ZgXgFbngSdJ^%m%H%UZ6R9J=W zm_K})KorM+>8IN<2WD?B&cU_VIi10-O;-Pm?We1nHQF)4*w&1R%&{}f!P(atW;+=1 zP&lyFwE2AeO$WZa_l0}+-oul{VzIyr(yMVPs698ijwq?P+8Ns`|hj(`m=Vk=kvY;>q0GL0`DS|Epj^Zd8Gsd7O%KEn` z%9?_a>m;zMs$zIOK*O?ATdt`p8kUX0^&q)FC&J+C@b7?k6%k?LdC7IZ+k+;FO-)tt z< z@pz0Vj*DCXNEadfO@8&10*_n=U+r?)2(gPYfy*>F01yiqU=_cc3d2xZY5|~T)W9nhsm)56!0M?Ag)u~N zjBqvs5oI2ndvB7AHBHNO@Hh(#V_8$x(!iN9tSAa9Lh{b)QzxF60x{p3cn;-ei8&E2 zE-zAvzh)RZgXgFbngSdJ^%m#tVu*cR9J=W zmp@R#FcgP>cy8?;X3Lz;z}(^}J20y&?xG`u(1T6x68>o$9h&xcvWPX7*x3Q4}eYd!Cn=!xbzO zg7Dy&+4gTb#S*uUxh-xVcxB0;(`utE@~R7&FXmvrFHH>i?AD=;cTg}7|j9 cvyywk5BJY^UKz0?dH?_b07*qoM6N<$g68$~zW@LL literal 0 HcmV?d00001 diff --git a/docs/ui/progress_icon.png b/docs/ui/progress_icon.png new file mode 100644 index 0000000000000000000000000000000000000000..c326d185e93a58a3f2194200052daeb83e209bb3 GIT binary patch literal 989 zcmV<310wv1P)ZgXgFbngSdJ^%m%Z%IT!R9J=W zn7>ckKorNnsk(8@F>{>R_8L2kal!}`sjAefNC*j9q^kb|5@M{{5d^bMTx2HpVhZA4Ok2a#Hyzwr&_xZl}?%i_=gTY{cXJoIQ8em>~0baH(419iZiQT<@ z^rBe0Picakx9@O%aXImfqc2~vySER<*vx(x26kHa=tVKUeg7fdKfAa@pC-7vxs?L> zK?pwx(Tif-weFV-><1zGlmY;HQ7i=tf)D_}7?T1y?T0nj^JezF5HNS9C`Bm{7evU) zL?AAR5CT64XD*9{fIIFG;OOu`3d99{K0J^DbzBd9N>MdUH0&Ax;OgdfIlxZq9*i+G z>>8?;nFC^s2|??Yi8ouDIiQZ$1pru91y#!g00d#U9NYSirq;Cd|;JH5Tl1kCO;KRz8W>|!%VK6=N)r*itTPVOsMm1aY)S?3TILtC zYMK}~D_xsb7Bd6iwbJO?c5aUlg0iZj7sXORSs*_MarEgRx4)4oi^qEM%)qU*zIeTj zo0AHXMUMj!LM8%bv)~6m{|uPFT)L)Vl@RG~+_bk=EmI0aX@as^0%J@%Fj1O_7RgI% zVPGc{-Q$zf91s_zswGgGNP&(|PVw=>djOD5a91}&$Z1dYOu+ozUsDuxTrZb0F6hnH zCa#;!9MEtoi@+EIAw)Vbxn36?w=16NNfX>uU>?0}ZH}jm3o>*K{WQW^ATH=v`T~v* zOx#B=jzs^SG{H>;=FTju0z=nwDdU5Tu4!0P6e$pIRfeuffwCEr`Is3vJIE9;>;z@C z1mGcMe2`gPC2(486bcvt;=@u|Eg=X(&~%Q@0`Uwb>72oR$R@bgj{)~-A`U%1IUU)H z(u?H>!4TZ_Mw&iZgXgFbngSdJ^%m#tw}^dR9J=W znZHxQFcilho?Ehqne3^n&b0g!fPX~66>(Jm34YA#NMWYTGTY8%Z|69ul{*4yV}sXT zK2z`A>+AQDmwc0JLkJo(?s#G{o9dP+Og&rT zeY-t~#c?dn<($Lr+tG@WXCdY?j$<3Wj9|5d*?gWmf1g?!hjN55Lg2aXL43zKY$dHi zDV64zhVE*$yF^a3jiX6OHuaR!EFMJ>e8gV|^(*E8DZ?!GDSOH$vuV~U!7?bjd<|P7 zNfLN@dp()sFh{!Shnr0y6X|z*${e-eG*ufxSME~lFobm&DncrYK)^Q%y$3)002ovPDHLkV1k_3?lu4b literal 0 HcmV?d00001 diff --git a/docs/ui/settings_menu.png b/docs/ui/settings_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..046526b2546214541f5c97f8635f8fb7d8c9bc8b GIT binary patch literal 37230 zcmeFZWl&zvzbBXw2o4Vx+=IKjyGyV@aQ6`06Wj>_f(0i)a1ZY8?m>eDcL=)u{O;WU z?7cg)HB-A)^I|4%(nX!8Pao;-k9|+Ls={fr!b|X% zE*Dic@ZWQ1bs6zzWuruYo;@RbCMPMT;bCx)4(~BCN7&bPMPJ&&!zF*9R!Zw%YdH*| z3@5{62wLne#Gs%+4#zM-#z+XunzpK)b|_uUm9{;8Iu|-@8F}3>kiK#*=vmaz<~-^x zyz|(4Ep&YM=rGyrdw0czY1MQ^B1*YGhUF#aM*|!55mF%wV=W0``e10TCN7GwD=G@B z25&Vfg@EvU=rvh@RmvTGF(!<-C_D^B7{6#dGdM3{KFbAvq=M9YUD9ulriD70_O5Nl ziumn(dA!|cQT%rB38O27ftXc;qmiuD?}1=+bacMKxgbEJ$<5wuBo&9pVY&V9`Hs`| zk-lPvApWl=cNL8I&z~XF#oFlK6_R3j(BM4QJA-#u+eFyS2eJ1r4gDW95pxm~akw3p zlPL)Esv$R@5$jP&V>W4Ga&;zhPQLLW^Eq!TLi!%=uGxlv5o?zjeA3(+OcolNGMbNK z4qIPe$744`*qisHwx8qE4(gk#slv*B3Uk~j$HR=jl07z5D*Y# zRUyBA{hEmrViTK=RAVoM+&n{6#STRy&4-Q6+1=vPWAHfk8c{6-*FeCAgo-NltI;*S z*@2|s;o-q)t^N6OlRG-Nl`>QLO6waNIhSWQ))L3gPmd2-Kk=()O5S2&Z?b`>Gn~S4 zy1m8Q6@vO=lVo;w)@r5*XL}^AphcOos>%Jhce5{+&HvHo{{DPy?OT**jnxE9Sy@^C z7b=C4%1mM3qL>}MpAJ2V4BG8au}iy5`fnj8_;- z;7H%>=Ei;*R%FJ8$rmLfXZh^bGUm-0bSRD6&1E~gmgd+hcD_BbzPTA#V?EXC{!8TX zhLVVwII+LhZUIv&0=vVp?IGm2^-eh)uBAmq_%AFvsjx;T7BMj@Nu>A1?xzSGhN4r@ zL*zzJWHF_usw!kOT_6+@i<-=Ndw9>)K)d0?)-@7EFb6L$-b}H!%y;?31m2^CYMF3i zEOCV-=9#k4EA?(PV*b3@r7W=*%|3tC7nCNLb!%uBe>#?Ln`2rYF4p4leaO=_thN|Q z80V-Ptlv%-a6fstUZs=>K{?){!bN!ghO!;~+L&^A=0TIp->=#8PK)^aDz4%5H9vS% zibXC5^B8?G#1hd2T&z~(WIlhll4Rqll>%%FWO{hla%Ey6uU@@kxGTYcM4(}@rAi;$ zkAC9|CrP18z|ISb89qLT@bxV>IBQbBo3hG{j*cEo=8%@pHp^vw(@z%?WobhYtnMr% z)CmqLPU77LlP1BQ&9vAjdLQ>#b@gXxRJjVw@N0DZ?R?3&XwrLU%P~*Km%5qU`1+6r zUN21&5|Y8{T;B1a{mJjhcJ}s(1-RALUsx}`fz<#v7PY0S?deg^(~sa-E;B$@g`I+e zB7-;*mqjaQSe>{mn2D7Y!eKEi%W5lZzsSj`TO;dPEQnS;akrHg96-BKFt#Mx%l-0G zeLz3f(m_hKB!nxi!*u&>Ye-XwV#uBr2D>$xibpYq)lX)f*09y5*_TMzr#^z=Y<`fE zHcc3=QYBLuPsqzfP{0F)gw^PySM*$k^+wFS&|%cVF8&)Gd~qY6cqPgrMjQe`kBYM> zFchn(>#|`wSoK8F;Yottrv-;!jZ`w*L9}fir{ai#1qB6XR~Dn+e#Fe!rEVVHxmkkQ zLjS73QH)5yjgg3mXd@+;Zp(P1cVIxSWi*-EG_b@wPiDA;U*cu6+h776t#;Wrev<-) zBxL~-juXKz=Odj#NTyLOUYC;*4|lx%`{M1>2(s!@eZ+iz_x1D<%$h~m6qJ-Rv~F&0 zM*3?Pf2LO7W@;2`G1)IGWs;bfNNpk_QXE>oi%W5Q)Sj9wP$>_J!M_f&DNFVUkBCs; zlJmYk(hT^sy`67E^BwWIGX-c;%zh$-E(J&%II91I!D>fiG!zSstRY(S#D-_%l_0J+NZz=D{3A90i0+uIw*S zPP#D_8ePVX+gOJSeJQtZF zviPUnLXJrgV{NvjtZ|Oc$th0+W;~h`VcXZp70Sr;zZWm}asRUNl0NM(H&oZTNKM5C zua;bz=7emX(qRbW=UOvfu20;8=~J{Ey4v1Fu{Vv~Z8?_lqn@;S=iXhs^!<8g(LtEX zB!^#NI%Zt-lY@1j@9yG;`*VA~^S?)vp62jU{ zN&v)k0i?qTaU7ux4L9$@r#DiJhuezcsB`$WDEf7P6QCI@fN}_STiSzSwucCk8I4Qg zS0ST16#I<$$SrP%sr2I(QEZhgKUqIFut!5~$N&N(c)yf#xQ)xJdY{~!jmMrlTopjC zS(IhJRJYfJ3#I|N5fOl0z3a@?NT3}6kdxB;H{GuRyo!^__`*#8tuq$qTSX{5MA!}e=n;0iPM-_yGz4@y@_wq@ z40Ln>I#m;SXRZO-!_ZFg9a57SK}UX2{1#dq1??0cdK20Y$kXY1Gun0Be$h)BW#$5#vo{j~7!a zyUPt6;t~>SKOOYbAxws?L{JzTL&WE@=NP5jTk-eLRDn*d4bH*g;U*IwKR*E!JoQB2 zn6_}6^+N}D8T)xWuH?#Zu!KlyXkbTR)0wEBU~=^8a33%IM9-Cu z?z~?0CmpK+3q>}76}$75rX_RKipd`Z+>hGF#>N=uBjUtGGhr1CAap^>%F6Ri?&S*z zIhA%9{I2w^cSlVg0kY^7;MZ&WPH<|Oj?7?{OD24ti(n!u`poTM{!@Fq=x{0*G8CVI z%~!ST9UlrIH%hJToEDLgkkAZQxnbMvhpo!LyA!#KPXLSw+&?^=-W|75f`#bjcE6m> zq#MEK{-NH$>lnb}ldD6$=|VM$ z()i)|-G_&p&DhEnf3ps@vsLWs?5$q$prW;$FZ-JtWGF$R{E}Jz$@DJ^Vgaw9d`S$kz)K`O0FvLIZb(t_V@p+;Y{H9f52wt) zKf7~Ai+=xRjoo56Ig3nP(srgOnDK4Z$DFGP8B${ab?58s({4XkRaUBPo?9tj33*>h z{&e`2KsK7f0Wa7HcI{$S`0M2ToBi@O*7s|oJqG~W4{o}JttyKbcZ0=N@~+SdyXN4_JrA8tXlG)4rIV3?c6g;uL(!-;E8U^#HC3 zN=B2hi{+*q0|GU8$86ACk7>%y!^3m1+C~bGPBLxKhhE9jdIJLhpQ^a50G-#Hd_^@< zd9Yv6VX+pG9W6It0jwX<6WX^mTdKz}cC5eAnake7NzyWuK$lA#*f#(+6j~LBv)=={ zVtHh*6Xa$POw{eN;dL5athZ2m?T}wFwga^0baP_-8$5C~43*vJlM$X(+ObFzDW@rf z&)bW4*T)Qw_LF&XyDKf;CFHc-Ni2raW@ctZ$P{o<;zZSK!=mVZLtmNp=9rn8d0qFg zlRIWPNk#_zH@d?Ioy}~g3o!tt&1+UTMNoQ+*3F%ZMBzwH^2dbRE3Ekw?XzP{Ns@A| z3^nm3+0#@J|5h!C_vId3gvxS0Mt9zPCndMgR5yWCquoN)0A{yLED7B@8Zxp(M*v(R z!bJRJ0v@r5-@9;cljw{hYPaN65{j1g^444W*_w-PYxFKyI~1|;v;vPw!aQ7s2~)u@ z*)f3N743U`BNg)E1fU?uW~S(qcgKTVrf|#hC-L??1UyX$%e#6Ay0ki++GgVEw&^gd zLvEJUpbMS^cq(oDUNqEUa+Sd=N?@6&?fYAAI^ms1lQ7Trq%Sa5PY_!uID76fM#%{$ zhXs?B1?WA?3IvcBOoktHVTd!)y*QEt63@*B&<@usQ&D=O@B?3>5_DHzf}2m@IsmH< zGyu(;PnC++;_Vs;$)#jx^SxvbV3E-5z0_x_g^iP0gn>gUeBIxa${bLLA4@_x6ywkY58ex9c6)T7LZ=E)i+1T82+b`x- zb*A>};A{pAGgs$PI~*`kuT5i0enqH63F})*uIv6a+-;}4m+y?`6$Xw_6HG2KYEDSm z>?Wu_Rcx+s2}#*arm!_P{~l3CSI4GjBA1xw5yt3hvsm-v?1tCxLuz1PAU$bL60H(K zG7@xKv5-ws`kvI?D27BHTWZ&A{RijN)gs~TYHu#hkl3!0wZkMh{phb&U+?8sUqav8 z3$^b8j6KGH0%zhYn0hCTIPqkJXUiwj|1dqK$@IyQH`8(K-(;&^w4+h}rh4hmC}46Q zo&D_Vj_Qq7AoW6y9H*LHDwl13Ohsy{a_@mvTWDw~|18Js0A_Lm0);KoKnAJeuqX$0 zf2Ai;{+zPfHkV?rDsPJDCRY>{JDZf=2DO9YRxdi5Pxy&itvxQmt*0 z+t1{6vw+}JI%+|W_;YIW=Ex@t1tziII*h#*m24bMHKDaJ*c<`^0#sj9ub9(JTW4M| zzbpKLB}nn6Jy|E);{;)lUsNeq^@G9Jde-ydTso=H6a#8{&gcc3Sxfc8zvLZfxZGZ8 z7m%@Ue7Tt1!}T;;$s<*1;11WM%sfLBqVMOIkLeJ1Mk3uu`zt0cmdC1&AcZ2LIKj51 zxvvjV@;o(3JA&15A2_4+gN#$o_}q*mf47=C|Ng9+Q#KXCiVM)wcGVkd3!0%1cob_> z>`vA9iYTXuue}6MUJCpqvv=`QS0xN)e5motsirlEsm#=fpI`yh8zhtf7>@je z1^mF10kp^f7Tgjd1DFyLrX(sT=_&KiAG;+Iu0I0YC=UG-`ageqJ;ZnnJwB+t2|*S` zZG{w|M1<$zV%mv=y|Y!4L^g+1+>w|x5@ zoIno9n8~oRAHqSK^yPDmZgkU3^F+|~Yaoc4V3G|`fwm7G7^O&*?^jKLmOsOS+a~w% zbrLw~i7|w=p6tvK{oE1Ou*C}p(9t&{cK4h6!ALmj{_%nSGJIeQAnir#r-!0re3+Lw z-GJm81K?E;c3FYv3AAc3b#-<9DI5g@A2B7MBC8L~ODxI9(=)IcY;FxD(Hb=K+DsRI z^7(sa{iI0sWJl!>P+h=K5(}kiZxq;_nZV*@{epHO%A_~m;A}Zv^7c)ECcu*j`*Y1rp*w@m!5HDaX%ToC+PWbA&aPk#MLJS>mR-nbGW4Y^t)1iKDPnuyeJu>m(H>y$p` z`i8^>D(S+3keNQjHSzrQ)f_H@T(F$ijF6DcVvF|xEQz#{faUxpsbA~7#OHw+8m&E+= ziR3CKnbiTMr0rf@px2JY@td&stDykgmN$@1ixf{HzpDfMZO;+E0!Vu?-BkYOB&olVT2>n~wE`*VI!Y9DjfagL1h9GM#018c z$!nr7)uSAiqhvs91(P&1Fi_${qfNPx^A+Q_+or<$)dKUOZURc*TNK)B>$mA}r`-c@ z-n$V8iHeYFl&FdcT#c_&Y7C5*{F!p8ddm+b`5rJ{INgBslLKxBroIf2tX*~W0spFV zTrHD#(`n3A@IF1GTC6z^<1W=T5q)Z1BqtH}%8NO}=P*-k+aF(eHGA3Cz4qz2OyYOk z+fDMb-Qg<2CI@}v)#3tzoj=u-+OF?>mesR$a3_y1{OO2!&uL?ThRE*myMSr4Otko%|NH9vkP`!ZKVZpYStd1tGrLesHduj zKJXts<(v%6(BHZ41_lQ8&O0$@kUwd;{pW(_-Ir{tYUee z7$?zCQQ^3t0`N#8<<$fNBl+(-a6^P6HPF2K*Gj_q!D;liM z;sal(s0@-1>=V$yrRBYZl4u(|X48K*O!YLdgV|suo}`@hQ9*sXTc#Ij~O;d)CLP8rf8qTo&kPs zp#V^U-A`o{To`e(TCk_*bF1O2fGxP13MeymdNd*6x;`(09rI0EVH9-y3sALasT8XV zx`Eh(f&5U?=*0rNl^s<4Ih+k&hYp?_Y`)Xf^qbJhPzQcJ;dY{+i+OFNbjg7!^k3M=LF%Z#nX_V@^1`^s(bnrP|2?Jb)(`6dX(r5WCf4 z4P8lTDV7jsoA2E#U^#XG3E*UJhGBkwJ`408vXPx$u=-5<_edJ<8)_xl*<~5ON4UpN z$nV!TsLY1GqDB*Ne*~~Yel31+G+hM!sXdt2`HCk`DgGsxJf9++MX+zKy8<@_{U3>1 zA3ueyw7|z^3X1(urcXVY}B%qX1; zt9~*-eo$@EwTX zqJ6y+?ubRZDn}surpHYp`;Ea2FeV7~cp{rMk_iEoMh!Igd zI{2{8NTu});n6sWhog<}3cFK|&-SLvf@hiZXxUH=s<7X(TlNM#6XDaM zu}=C;p+E&z)V!ZNo13|qv?Hlp58X#P!}z=f30zXr+RjcvbLXcQ@YH(aw1q1xEA@U4jc3e} zI1Kqy(yE1ZzCVcpWSh7m<#b6t6W$q4&3@3|cOxFpcU9o!<0mU4tE>DIMjcJKIeWNt zv$d<%?0No*DU~bO`KeX6+-CO#qyIDc3kyF1O@Zx{YRmQMRk-J^=O=G_BKEF}=gQuY z5N~<~BjbppH#t}>7iN3eZ|!ldH8!~(<`8dQ9WL#Te<#fMQVeo^U~#(dMnu8mg++eh zqt^P4@6Z*S%WI?BQd#>dliyeRPWR8`_k_^DyQikjo(}|kkC%9!9V{}jp9EnZwsTE_ z&-mE@7;48v0N@KBdAZ6Qncw{=3c%dG?)~v>37s}SA>E%2(ZGz{=M32nK}UTXkRuhb z2BfLMF8MTWraHUDk3b(=iycZbo*Q~D?7Wj@gHsF(7x(b8p|NoDAiGPX$Shdm*kIRs zPJdx$r~?saFsX}&GQ{?<*>nFDzw2SlpEop{kzSdUKF+WfW9bDkjo%d##iz@x>>eDN zFt>HNY+u*6rmHngPq=q2YW62`&|DonkeJdacHYip)3bfIp0XJ3{(FuKsUdq7MH(U_ z`H^IAaxJ(&fi51?P|0-m?^Yyk{fA3>3okEn`^FlCWD%AHQeA%gFc(Z1YA8U%!^8{# zrh7quQQWxCUeQGt| zu8W8-)C%|5y6Duak{V^BSyZ^`uzYp0E-^1~DbX^v-)VnBc6m0cKatGoS#;*%+!s~~ zz^fjWqWCNLAN#W=0qz8Tc4jC^dQ%>VX zMJEw#2l`E7XpRCf>oEic1y2TOGL0^FCmK<*Z%=qrgzjYUc)YM%)7g+ys?B;gWn$xm zC0_b$3Zujq_(lRCY}#8xw|5%^Tlr@$dVOQr(6;N56m2byZL`8blNP{;Yh|%-g4Tkq zRdu~ZT>BcYQe`$m47o%X*uP_44tm^w|8>ERdwWM=J6m#$)BUDE#q{k-4#)$<>(4nZ zYXqN0CWVjzLpNE(vFx>`i(jami~JUbf$#4gGPm+&GhM zHCM?Y9ZQt`1xwK+s8laHM!zYy$sTvU#x~7NY`Xhw_wclEd1(UCeCTUWvZJ)s$nS|- zg=-Rdt3O+8!u6g*T6W7d_-&a21%l0J+=t^Mb+&?Rsa&BXyCkr;w^>#bPgUpt8Gx7l zR{;Lx4aY_;6Jgc%{6#JI-k{DcaD{2Wpiz*JIv2o1-rWuRz!vZ{@X2j^7#|iMJ{Pd$ z_wV1!D$nW6sndF0xOL~9nzGsVGP-hF#POPj-xJ4HrQ(^Y3rSi8_NiR7FKRMq{6XgE zh*Izxz0neIe>V|jjoq?CERef~3qg>%TiKmxNxRwt3*?Uv$qm&)Y7LW?m9rl?X8A_T5tCe7DrJ5xBqI@Y!Ly#s57*2ZN$u?&(e1SFC(~qpt%TrH2zx;v zwp3>?;o`!X%HtRlg8DR&L;X`qO3L`|>aYk3dFia9wkk8qbt-cfb5B1TAJw1lWER<( z9}eOyu4D1DbI`GWxM4lyiqBWCf#S|6oMh0R}f`vuhMiFf5mJBrgKYC=?D-^(Gawos-5bnG9NNpCd{ zkB-V(p2y2ak$fvCpgCG?izN~A$^zPk++pgP>y)LJ$xz)f>#{wp-}pp3_Q=?3?8B-9k{{z~3uqta@KpzbY@^Rnty z>6a+cucpor@-ORVl>iuMqWNtJzvR`{6vg=iOncw4@F2i7ggl)e{d_fjDF~%9mhQI?#ENZ3Y&qaBN@=*v>5`%YIwH=!cna& z40O?kC+#^7y}w_glaxGQhUHz%^l>o`$E2Ch_6f4*|C?6gF7|s8Y+da#-zBcU zP6X!lejB^xXp~l|ZnoRuqF3nW1Vl+lytrH$2F$h+z@M^;j2wvnk?kapEv52UR2l#p z;Y?4Sl2PatGqg+@rj4b|apVWR+*XhXqQgX({u zKLBDuXGu#5+D7DnKQNh5n&U%>#Wz5M2Jr_GKwAd^upQ(D*|#C!is@(o+xZZ}9st^} z!~v-h=T`qp4%p5hl+=97jv@oxB^X%GHJ_ss%>U0R{{O#G|6^RI64G6T0%fGZAI0EK zf&HXYX#!XFuHj5sm`Xn3V`(YVhwWi&W}+Tta1i6ij~%e5YaM~}&7K@k8xDl!00JiH zPhug3QgPOmMpuOgK(1k-1{SbhChF|NLg=W2y3io*fUUNVu`y3XKbOg z#^?0!4j$K}&@2}~PCaMintKvVGBHF}H<~YaFOGs4Q#fqP133)0Q&EU|UF5|HVdG{1J3#-oE*gNM&y-u)#jST>292|VnGJM^PIy`hco7gUNc=U9p+M|~%6)~kYS?`-1V74}+#dM776o#2XNs&=>rnd{jXEee z9cN*?LeVB-F2Rb+I&Y8n!EEdkcd1^sUCLykesTWQWhgqSN!ggs!Ado5xk1ZI(<$uw zHSNkikta!Y|8O^`GZ@*ls88~62>BHprOS-Xc<~5eP%yYJ z=_*lIE3}5JdIo0@460x;(mE4p2CkJ@WN6HuXa%;1Q)7Uz+mi+*swdwzqqUgQHsfPO zdc)LAHxNz4xk-3E5ORBCx>6GvKC?Kkn6;2@nG8V@-G5f;9Z?(|9pAP2>3b#tvqWJc z<{)Avnu>hL>2i+?n@JsZ^x<0qO;;V84@YyeZ^HY(H@9@hA^46zJ6;%THdHmr-8G?g zg0yJ`r&adZJ5nJ(l%*pfwI7p~H*OK|T4e^gW&5+Wk+Y*9E0!yf1F6QZyo<=@!H>Xe(dr z8HWNkZq=8y=IP~GY+GeEkRzDudv^_#hpN4PA0S6C>Ndm1T(zjytCHKd{oxRD+CF%@ zRa*te@-`x@bg9BiDlAnboBr9=I)N$4`p@IA?gU3mBU=wVfXZXCyJTpMzx_SbIDGf5 z??+qYmBdC;ZXc`FE^81Yns_*7PvUhFaB?ND4V-=7p|fS9eaq~-po^!@+iiKB&h*c# zR@u&^SC{3yGg|oeylqqYgDS~?@E#t1yF|M(nSO{GVdK^+mPVD7K7@h$pq7=%f29Vo z^!B6#5o!4ZD1?bj3SI*^NT=yTFy^Q89hUY=Lw_Kmpr;GCwF?@G*u~6)hZU$9K$~61 zVUvUzglelzu08sV-c6Rp+|^39WJqAaO(yX6a-BcRzuDMh_-MX1E#l7t6ot+qn0#eS zN&NhZUI(&7=}x~s<&oPQlYs>n*tZ@4hfDz&s~`UvM=Fa>AC2q0%6vT-ba%d9^1O42p*fD#Pt9quwZP~=l^PUMNV&Y z>$~_7e=5~7R!QrD*hxt-p66sr)d>=~QZw{gMd+JpPI~Har<31BrW<{>N2O=z0cB#5 zlNo}lD=3JF9e|U+`Ta}3*#qP8@v)++Dj+>QU0Fb^d?eplTcb<^kp!#v2b;$f2OX3$ z?BNw*NM<%TPmlCnJv=_(3Aw?De}&Bk;@h5k>+?@x^HrKu94}FcV;+6e$7`MYU=K~c z4XymhJWF1FiLO33oXUX<42+0za|M&jKa(x+K>qpO!n_?xpr+quLG|H@P+f#(EcHATiOS2;I6djzjv}NRREBaa(FK}49*NE$Uc}L;I51vZNAILlYeMKo`&2Tz ztJb9WTVnI#x3K;rXU2+pK?TPWTL{wGH^1XmSV0OW&n4VFx|ieMZJ;}Tq0TAGu_8iA z$}oXC2Q3)|?h!TSe3{dW1rDk_bdqIlnH#B3kMtOrKPQTF-KCF1d7NfT-mWr)3AUmW zbF!}JM6tYL)Om*07d_MZRY$#t#prpCn^fS&54ixl$oV`$1Et6`^B6vIj#aSACa`j( zWLq2ZIDR+D!q!zn)~SHl&N;TbW32Dtfuu{y_V0rLdM&MF)~hyO?<6wi3{Dcmw|Wc? zZ4XxhTFiz;zmUDYcge(v52G`V_{d>MhM+CF??s4}z0jB!O@+vYK^K2wGlz@vnNZ)B#kf+2H1$Nx^$=xd7+o9* zAzWOFP2@b8&!%`}?=(1QS}{8BlK(i1=Utf%k1uG44b2>Tx3zAgYZ6dzvPEyM=knVf z;Vni4)sQ%()>p5j0TWBISBXzMzMQZR%K|CKK1sPKNP5cS!a^}7!|W9ARNv349*rR9 z3p;j2vK$7r0q3d02yc|>q{M6jL=EDpXaS_2hrOrNx{phr#jNJ?T}v)sOF;$#2GYTN z@1uV{x%KecmoW)kCmSs-0?6zJ=jKul_7tKkp#4YF%)uO!KqZ!E3hI`n3In%n&+3X9 zoh_eCJ*(-mrVp&n8n0ZePdYu=I6-z8;%w<%MCm!)OXzIfkCf;-^P1Wt&v81B?;Fvf zDV5p0<9*%$JtB59E7>e>IATt@|1+J(Dj-1GEnE>N?}jZ_qT-u+yhlXbXt}%A%ul-v zSLE$hGI6Qh4=>Vs>OSREiWjrY`L3EBus2t}+T5{Qxv8lhLHhFIz?9o!nXhgq%CWXh z*lz>&B??ghkd)|KdApps{T}~XmGBlzLuAn0r%0Z-UNoyf{*-b^FI9-WKyh(W9$iu? zduWF$en0J(LUwnpI2~7rPzkxG5$*q5=3e0!%;7;Bf=I7FEvk%P=)+371Uyb4+hgYe zfp7d1^PE0Z%*6~yM)yry18dg6{M{xui5z?rYAnwD-14|nY+zKWYuzE5`_JrCNm0r$ zwjp!c61NuO8n$i#`-)Qg@K=?^TKprIJeB5-gpAEK-x5mr06X<18h!`}fEgbhyhRM* z(JAgvZL2L7uO-oH@03DV!iyD;_S&hLeO@4G`Muu+%Qqfluxq6^fwshBQw z^j9H{3pHXRr9UiDF3!bzkZ`6Up?>!mT{2OV5f;Nvt2ALz9m=WfO2i#lRdPw!2^W=$ z|5-u~j|_ zymkIUe(%in-j;PTmTr4AdZ=Z|H=cM5nV5J{*z5NB4F+ad^&o9&viD^>`V0EqczpU0iP8#&W|s z-Zr-mmF#RO`WsW5s$uMYfaL;IVxx$KFiCg%F(L`~ky85Cr?^^(?~0-X@gJT3Mw|tB z1|3)#h|M(!{G+iI4u2=!5V-S<^5&OXwIK~@nZLu zze&`y)R*XV`%sTj2Mq2PDhgre>mn>kk+|K(@w#lth`oEta> zS%5uj94~1QODvk_ql+HvtSU?+nF0*4)(|Exq4h&Fp#S2cxWN07B&c;<6%GmwrTIHV z>~_2o-QO>($pJDw0wBSsl2j`P%>o38RzWj1))OF2ih+UgoCJ=`@TsT#aRYr*$Eod+ z0n}nai_C219(N}At%2r%@Z1-RHJR)K`G2-}b^Lr2IH5g$%tpY|fN(|T{7dpk_RXKu zDbk=zV0^2-5gfAU3#uv*KIuROk1F_o`g{Y8!ACSva6|1pn*zQtxppc-Q7Mw0RK%W|#<;RBFxf^9+ zN??R|!Qgai6+?NTolF6GeY1>HDF<#wE)Y~Wk=#-=K>L5Ei)dd9xY-^d{3h&60MvsX zkY=Ao*NdZ&QY};^$7a&S#-Y~`P35$Xf{}#Y%e5{lpn^5KACqX6=zL<(E^ChW05z#b zwziSq_?-Rf1t=MS{ocL?i$Wa+BEsDu_k4P`rGU+1K*VJ;E!yhy_p^w zYB|?H84W}|50aZTHkUoph>zISLyuV_C44R}0XbI606NXQL^LA$&M zFs_rHolMhdS+Fe}tTf>cNofB-uLXKe7D#8UjW^*|nT?jT9QBX2wTrxe&it-92yn1$ zwXT=%|E#L`BG@cmq-4xj9X5HKdwY&bju6bmU-+$gTLZ&yzQPF$s$W-`51O2a;%d+u zGRr+|<)FuYT!A|UT4*wdC7MB-AF8&tHV%WL{AT6i+?;lJVOkm?P`eWo9Hd*eZ=(r! ztykf-5*f06o47r$F7RP+;eHVM4&=b?wf!Ckeq6ahTR?p7iX*^Sll^`CY2=Z3Id=`7Le_LpxiEl2SG}}r7B!4WS{49w(w?e%FZsv#6P+dYP zr-Q<{y++%)^4x+J$|B9uh%5W0kHavp7{$*mlvsUsA%{8ux^{NS#Eut9o$C+@m%o7r zQ5@w-VHR}#AKS;)bHu=}?Jk)*3~&jMs-W}YFzqY)+BiRET_d3$dHQ+Ke5 z5zOX@I;;Oj;xJDTzb<+`ZG8)b0EYVJR6KcxJ(^HS5Q+yA#q@6f`%*?If5vGeNOf5q zg0BT|^Sk`Xm+WkOOwy_If!l5PmyV&H5v9+w1Upx6=Sj;QWZw;JJsXGUP~I$m!~6GZ zfWPoZziarAqm3?5)EYzyQW;RhuinW<031pf)?%vi%LBg2i}(&<+iYF3SWoiWsz|h!$5{6k=LqO&434l zMDIUbB=e);GU`Cq(ntg-A-*p7l2qxX!D!xp)0>Br(m%*R#DhLfMCu$y-R zi=P6N)!a0mK25_*56>GeS6Yl*sh>(mSFpcp$0Lc|Om#b-WpdeDB%Ufz?!~z6_Iz;ep5yc?{4ZIK^i_q)i4Xs8;n6AOaog%Y%Q%S)ngM-5PBthj7mD&4Wfv*@ zAvUlRmPbSyr6%9Y{`x)nMUe|xVzY38sH}6 za|5|jen{y1&F%5I(PH*n12o+c#H@!|tdevjq*~y{bm+=vI32ck%f%3Px2jVl4GBdf zK}d76ueHtjj6?TMXQ2-^);H7drq>rw_mMC%ci!dC>6i~2dfE`feYmzkWY}#Dk$C>%C0b{Ylj%gor`oExK8IfPU%wtB zaaUTMWr)q`%z#W~7`qZvQFMWy%KeuBL;*xkCLv%-zAV-XqIHbxR33A;J`qn~R1=RL zK@S4{w&`IJ4)k*Y<5eRfGMh(r`n9}&Z_(k=>0&igAz=z2D8UO#%r7m#XDO=A7+tId zbG$q}*5Xx!J3!H!D?SW}$z}^?Aa3@y-wv_Vn2u8l`d%@9g=3jS+5lv2CN5IRIpI7- zfKapee$#v$EBmvs$9qHKbzHdHVoBL$>GGHr{Fcn%$JYK-u6=K~xW4P`z?Z#*k*7UC zX*1fW2YVZ)LeUKzqbP`ms0wn)eYVF)OXw@McB&p`qcPiF8u1On+y(-;wc%}}Cj-ul z@8dSQ?E%3$O@1$0BN4Rw){>(KIB6_#H7ct*gDRg6mzrRm&hk!g25(8%qn4C2`663K zneqMzD^lt90w2CdLtLc3kgG0u52_}G~*X|8#dGnM`P4gKeYf ze*8T#rzDm6u2bJVt67yKR@-z`7cX`2fiIP0DTsAd=NlGF4z-gz@OmB0!Cp`M&a2`W z)Vn_kOGo1u2x@X?ygb_;K}IS#Zub8oH5nxO4_VI2BKrouR}vEwL#wU)bFutC1dtt4 z#G(s!Kmc5rNsoF~fb8G0#|y7PfmWlQinpuJ(4(50t63zWL@q5GBAa_%K_eolXq%q= zj6=U)B&T>HyILsc%*(gfC*NPOdLtLFiAn2WEL!3O)FR+aSKJ4)vCp2eO<>fT60-N6 ztYKnGM!*ZH&C)2RaEkRHnAVlK38~Et(rX>4{7XmqVBrp2s`4-D_pDi#q}6Pjqm@FI zKE)50r&u(sMr#nplWR@3P9RNeY>()j?lI7X0Z5~1jO4X?A>L5nq?a8KY?3(huE9$) zSeAD2g8k1S2R@Y&LN`=}U9Phy%*oB2X!dmGw3}C@xm8nBn{V}PqF*rn6T=+++~cd! zxNVpxWd%y$3UZG)mW1MNKhczg8SzR-U?{79MiSB@hur3GqjIjuaDvz$Q)_I-&4cy5 z_JUu~+z4Zm``$;x6LRv;mPliv8ocb!@brp~Te>%R3+c;v31c8git`B~nIDIpcE#hMfmN?2D$ zLX?{k_v7pELxWoiwN9D_GRAWk;R4qgEvu%>lIQ?XepIJY*gMnuBK%4jtRx->^K6#~ z?2$##53XR)^F=R6si>%2CWwKkhAQ}G$ZwZeApYxvoWgiRE-Ls`Of)?Dqk4E?dC5LQ zEG1pQ&Hm{o{nFqE91Cl%zNwx_7|qGxzNHR_u5lo7)mRhX(V=_g;4ma;XGBP>K6>2W zuzkDZr(=l=9`G><#6M2Xz|ZtpXf|i)oGpu>QGYT|sm_vd*P{{^aFaG(eI%V`(~QR> z;PYYxTX?Lo127NFe%-QiwSej?)Vqqw=~pP!&Sb&4UXtgkHl98M&xKebd@KG+|> zPMTCs%aSa!o8k6LBXh=fj;>emO1lJdCn>5@3BHa~lu_x;;=RRp-u1gTI?ZS~zM1nf z0NaW>u*&jDHgSa&qnq^^$Dm*ahrs|k2Yi8F|2H}gugl%C71Pz@rSfKz*-titZ(f<4 z_urqhv=weRf5p8#T9DYej&rH8gCdXCxR&PT_dBD+TjY~(6qQW}&=lN3jb$##mH$zY zCx}P!IXKv>wJoxUtz-{15Sod*_6^YaQyno#2`z7i@$pUgtX7dyGLX)|v+X_h$xxIm zIa{glbRg^m^{KgcQL^0gT=Oq7>91n77XDKw^=g!yAsUhE1ChHW zMr$o&6;SbcQmD9Jk_dSlBX-b zqA)qf4#f-M2mc?Hn9Col|3``WKT6F13ONasys3b|9ax}jag(T(2!4;I6G}zCk^-Me z8n3n0HXF;(cZ?MAyXU>V`168=g#{`XI-c*Et`(?cnt(J&#m}FyJU$@!2NEc;;4@CW zUiKi646TIv%J?=1J>E=_+-L$Sefh_1pwHS3OL|gJ> z-5&2fslvd%FIlQvd%+gG(!2cAkrd=xf{)vtgja~t`CY{U6&f9TNzVAt50-AcbGa`}lZw48oG4V9=GG=N3K}6Ea^1-xjzkrSs#g`d;e` zf?^t-0%hWp%ld%ADalEwdMDLh7$Ez??`9o-eqq5F)OPPKR2R7E0eK`85-tM zT=?PQ4GJFXGf?}eHm8`%i3rNvv@@tigGQ z)p13TNYDcU{pwZU7`O%2|5tl&9TwH!?|UoKp+k3fm(tQ5N=pa=BM1Tl(%s!DV2}zZ zDIg8fh)5_BQqrK(t;F-0-+k|M?!BLVKWFdf?0?R6wtsP5u9;agvu3UJdB^LUSOla8 z!xulHX5#`p1Hg^Rdfx{so)Xl^i2eUexA@+n9RflE!C!EMA4VEe(BUiJ@PzI}Z7WLj zg2WCG$S{@TDDn={#K#k5&ME9)RJ5I&l!ecVI3`R&NoQ0FP!K&pO{jXjtbVY|-*%d> zxCi&x*jKOiL=aG?MGhumR9N>h*%Vw#ok0P*z|HFRDT5+HjIr)G$5AFiKom`xToKKF zIQKFxDOX`jbdHpF$UDa=CTLSRdw3|%HhZTR)mU}o9v>g)!ck-4kTkx10SO$+*gqHN zM>1)=riL>UBw_U-UJG3~tdCeVGEk6%?VVowP&DILnv@$x&^)ks_Gma)4zN*D*U^++ z@+jEHywcA8jyv@2I?$8Ezt&&|<8HC0O+!TIoLYwpg1LUPp9n)_#0 zlSVgAaNjEYv*;G%D23BxNqCF|%w=$~m7D-bRaslBwNI;D<$bct3ij?5wycSXBYb7X8)dnE?X6RKVB*}J=os&ZSG#*OcrOnro$YiIc&S)ySGBH<^nr)H(HQd2K{hr zuA#uYLuqg1=D`&bl2)3Ly{n2Kg9f zGTD&=uiXpRrM^7Amzkxeb+=|l_cIrKqNFxQOn~Uy2|IH1D za8W}1ehtw>i2BIC(#%R*eYt~;G*@9qnD*_L`&mw4;o88h!~UGmPQ0!cu&L{WxDOH#lY} zrXBGNqfL=FOKZTH1T=Bdv3IdQ7}2!w0A}r>129(><@oN_YnwgMMjDzRHLOBD)IX4q zSDAsn-y+lyiVe%MV>EQXXNg&T8OWJfSR=HziY!@Y{ET<{<4uK?)<nx*dB|EQjK)nvB5N=Na>DzPj1R^Kt zTGXPC6j5XZ>>wBEPp2fG!hSGOihfOFc=?KfEp0iGJL)b1BZ*bACbakATRLHzxR|{j z$LO+=yH1!@rq~o_2+E079bOyf9GB}cgKw=r&NeCRKZMx4O3$J&(gIT9@*!S7$9U5R zC`2FK4Z#@tB^+?-mELY+BZ6Yz)y|ud15$>bYp~adg_vYud#v1y8lPBT@3hD&x15&px)Hq2X)Wr}Gh+yj6wjx>AZ`;X;H;6&Z`{Mcrn|$G`h) zQ6LElO+;4veO|^z-Tlu-fqf=7>C&@k=BsqSQ1m&?wcdn$VHtYrcqK_>tqYI3O&qrk zwS-&o?n-9T_e`nmRt-d!bKdtI+d*d076zLN@BJ|rlQqVw>*14DbdH>=G&xmDHul61 zH75jgRH5H?nYdr(6i3Et&X02D2(|LUqpDgCI{*2WW=t9sb*GC(%jyCl`)A1RgaI? z*+36J+< zh<)^v=xz3e;LFZly1}*@&oPdBS&t#tQ|@}WQVhWsyX-aixs3g|5=qC|q9x*!w_m(o zq8pUuoWh3AlMcq&%S{KN@$R=Y5llvSR7X>h6xF51)vwfRhGR4Saf_Fn#`#bBdU1@8 z2N9PR-whlJZ+>JUS$qLxn3v${)r>y>YH6lPAxQS2o@FuH?@_jkUIxBk!%K1Hp8_?2 zfVBYLQxS{WPz#q)+7KsLq6E;N%><}ZzM;erqhkru|^0=as*ktgmpRAsCse%QGk9s*on)Oxq zdjR#U#tx|zoM1sRqU^|!w*wHq4l+hPYpf#?Gs=QQhnG{(&JagglX&o~HdR_ygYVb*+;%Z!K7JSMz{iGqZkB)J zyt&5zL(Eu}Oy~)0L@#zZ6o=_z;^hBrywwDbr39{|$Uy$K9Zu-Uf=J~W$0@SfhhKlc zuT#z7hxZ76?|+KYpe?oV=}XTm=InqO3ckVUi{L|)K{PId7->5CzLUz%Vepy-vb+kl zuMOgC>NJ7YCcBHXW`+RxR4nvBzaRV@cXoF-sOl)ZPWQ&&-#Y{4v8Ts}Q zP70}@GD23qiS>9^h`oS6G#NGBfOq!iP%ilNq{?!UjpomR7%H%)-5$dER!su>y%u7q z0ocd)UzPX()jUi|rOmjI-chHKk^+KMIsWtY`+EJsy651P6X241RX zgh6?yEYf~!^fnG#@3#hSs#Lby-8~2PNs`LXTz{8Nih2JafwJ5B{c7mw$-m!5xmgf; z?+m@Db7ix*@xY1Z;#-}iL+3;f5t7pSLC>l6u4q&e!cNa_E-JGMZ&H`b(L_-_nrVas zr_;y=zb9MJEXU&PhR=t_`vo(8A1~HegfovKA|rbsa&K<-GF&H+D)RRi zvDY;ybJMz`OR8zCL=TI4yqDAnK`#@4e_=g4akfaO`Ba?x=UBO1=xrH;LBSzcMlg`0 zDw7xtG*_fGgK$)3bB!O?etZs7YGq|WPc8smheOYDI}~V6;B@RY^2=Lba{f9O^b|GL4(Za^?hMa zD>TjAB^;7B46h0c8JL-`G&VL4kBsQpxv#IUqey5RlX;(M4Zc;c4hi0$z{Ve;M#qw* zUPc_zOCeNFVo157#v`by!vQ`^)e3HX*fxvZZZ15iu`-y=RvEv>wg664(S zN&bYnq@d;Cb^#j8AiA54i?b~@`N;uRG6 z@`#e0#2lHFI*0IFKAToZ%I< z#7S1D^WkdxCJ<`wOqa>440qJWk<1_RdJ#lDy08SdwnTx@Pb3s&8$6M9&O*hKX&O;8%AS z8PqPw2IHg^GjNqMJ8i@KypZ_lQ^7A`?|5F58d<>PiJKdJcU-y(M<@x#Wi|9F4*XTY zzxGB+D+r^iIC|F${wvJ{jVJ1*$Il68c*2e~X+OlNWXSkdhV0COtscU|m9)}&k#Llb5?RRc)SC822*XyR@bK7dj>KNr z%@Wr4Ax01D5(pl^R>kmevKu;di8unmx*D)2qmO8*2VpB6VtDw&d?`Bkz+Ps+o|Mc; zh&+R>^4Z|wql(^AsNXR!?1^rZyMq{P^?%$Ox%Hrh=U=h_|GRHW{VoFFQsH_3P-;M8rwg;P6A?UXXAkp~^KdOULk;HRgf@Q+%(jH5aA5 z?0BvXGu~}ufL>UALQOuXD2ios6DFHZsIpOf# z@%|S5HNf0_DZ<9amFpQa^MVGGNsTSv%KB(k=;Lhr@wWm!4m_>vGkBqH?jx@aG)7O4 zkq}QLV$7TGliHKyx>F7UpG156AZE^J+0%Q(ClYMA8K@sV3`Yb|Mu8#^5ztJT3^Q|- zdw&E4U1CHLAy6!rfJV~E8?Uh!z=5!^u?)XOD`_<`%vhh19{!!wa+r4bS}I%I&=bee z-T^C`n(I@@%yWd4_bED~<-EK$bEhFkxO?)rZlUAcWyz;5@*Kn>k2*%?5*WFJASQer z;p(WN)!B>@o7BvCnSQ*O(Ofcc9<66mB3I?;5rvd(F5*j6pFy#1gODLQ)^3%=y{0bm z67FCtQpMdvXxA&DS-%guyaurBU@;i*%)DYq^lIwr_(v3Dxk6OU?<~AxVPWm!4(Il9 zq+w!WPHyJZi5iT|hUm1A$3{}>azk`KKC@Co~6cb11s(fnr3+7p4h zqjt-_R8+iSktg5h6`Flch;E!8Qmlu-9Y=^)7&@Oy(>q6ftAXd4K@tcKL%G7gXkZp_ zrY|>ZdfoN&eejiuO3N$I&y9m0Udi={1w49U(r6Ua0+Oa3om{5xc-}dv%cgS~ppsXa znWi0qS)vji!~J@2jg3t$5F4ZuJFZqWG)lM;nH61C`EmL)mE%-Rk7p`7=SFkDBBpYJ zNKeO!>yGh2t0t?Nb3xamuB<@)&H?EnL_!7nSw%92wt;k9W!~ zs-9fy&)|;zz8rvYKB8-FviIf|j;8@JyKexr7^{cV>D6 zPDYiL@05lrTEm{ciS_dRlYl>vC3(-Q+5K&|AvvEh7ZIBdcV{UAuM-7;nUI#x}6V z9*Imy{rV;_j8dJHMXr+VLzxkub-Up8U!gVML~c^;^|j+CX}H~-mSMJ<%D7I?D|y=w9}AAZLwB^w<%`W}z+vh3b_Der zGHzt8uFE!B)H}BcLG(S5P3!!_;MTdzwO5~>#k<>&=e8H+aII#qHXz9P%oUAPhp{jO zB~^%YOtBukjDIb(5}=t|Lkyoc{7!3cE_c5lg6o7WsPpkX8Wu(W04R145K-+lf`XG$b=6B_u8m zf9=N)d6iWyvf5nUn>T6U(oM`Nso$uVsaz%XdiBq*YPSA@o#W`u`h&KM=(p*|wIluW zdlEu=y7&7FqxYKnxJbAI5c|)&D)U^&$ZrF^Xl)_YGD>e%o(95#ut0Y|Dcx3tL z_bs`*zn0^3uF=Z>%KqB_mbzd0i;sUZS%LkN!Em_&|18-OHh3dm`)fKbTSBBLWE^)} z&Cc9`|#mPo&Ae(-n+lTc{yb)Ql`A{X-nD5bw+;>z{GMCO+3pT z6?k?)-jkgrUw!tJN0_$VvB#L}W)tYU9TU^?UT(}P6b#%y8&&UWb0)q?EuiqtntROR zq(x}Fl0n7m4DgMx2$lC=C)KbKK zy`yG7?s?{hAiqjO3{y{$st-rc&_{(|v$fnWr2M(MD&K4xi=?AV zg#lz8o1}^1^XJ1~lc9x$g?=*AmH>-V)zBcm<~ck*t^&}|m(2S|13XbFPM4`+ba*Zs zjm=cH01XP&TemP-(Mb|7@>K#y=+pC;0#5Hh7CNt}C=wh&a>=k$&G#Xh^y+>D#2ho| z>Y4}~;`FSpuCAa>mwAcoZ^JOMjqi;Pe~*S;!pqNgVBrsi`J)3hm`Ewm z1;fX3gYkVxF&p)VkN^D=|MedfD_<1UjkMq9HQ8?vQevF zaOk~?%Vt=yh6!2Q1a)N0Hn?JqjEvL+|)tXmURxSo3)^5JfLkPxbOwPf#T3Lx~+!*p)n7Vz-WuWU+J35Q;2k4|IDgiYDB0Ic~uFV*3nDC{P2Ybf0oTv zM0WwsqpGH+BFs%AZGnNd_V#Uis;9pX?0cG?B z+$8O1gHoYT9LPoXUUjai>n;k!lQBzmKaenmOTq%y@%T)M-{86shh$ zLQIN$(KTM`1SkX7y6lL#?6S0;|KRqM>Au-}LlTxt$j&s{DKG?{-I~#3otKZT zm)#=+ZpP&%k&1bqqd{FGLRz9_-u3}^0D5(uy|J+oEoppc+bPp*tXcJMUf)d_ub|PI zjS0wWwYug2YSfgwf>Pu$$@x*+l=tJppK#-={%JLafR_@jn=cBW!V$1s4RAJa*xA_$ zxd(0+Al+qUgAgbX@F>J}WM)`eB3j?ukS*rEoTTgAkNo~&ksTWckNn6R z`l|*{1IWrB5$hEA5a3gmMjGXgYoiT#z@!xWG z9DYq*+d9ZFRokQA*98Jx2L90jM zk!uzYLURr`dZAv8x{uC$nr*S28{99by4~#O*L?#_dBPVXFIs#-Sm1WK-|E4OXUWZM zX>FOHOhC=`2X!y>B`d4L{v{b<1MUY?Jp?%|>&nm@+@RmXRMqtJbuV98&W0#WWT!=e zIfd*f{2)(3&YxNF3~H9|T=maYX`~;M@DZ~yJ+u}Bg`BIo4gBj?*DccV)kDPI581sh zg8sA5jKhEPxI8#uLEXjt1$=l9S6H*AbJuB3?0$&bSO8L(Ldr`3&W8p`<>KWpGikb4 zI7G(EG{_5IZe7B;Z={+lc9_h-cd(;$_WK}=!(YFQ%CI~E#nC{tiI_yahtV%Iy^NOT zmxHC4mDnj7T9vprcm5rvD0CfjIHg@>#KzgF0v<|66m7~AB68yN#oc7(p0$}bt=B)N+~YL&v*VcQ6=x~3AURGth-lJQ)6LemH+ux z(zuO<7*+PhR3~x_Ul7xOos%K6j{73}zZcXgMU9z!v9mvP7?u6q!JLVA}8Zg+n-%pS(h|?M=@C63q z0SX`t%7SO?{DcKiaxb@>UcqSuXz7T`t-3i+DUfh*sEPh&@n^IER z)izq1&n0fin9VjS&r=R$3le<0I59bq_6{o&67D8fWQ-~?6^$)mz0s}1*QBy@{DGLU z5T1rqM=s`}B5YQe{!v*w&6h>OVekB&vCNfdNGbfBWCcrsn z6j!4Otrk^(WjrOBADTgCqjm9MZk9dDb#D-f=yQ7aP-hv6R7V{>0yJ7hTw6Q(iSlCj0scd-bho@?EL%-kv3a zNGTSbfRv9@5&;V=9PNZ_ZJJ*7F^7C|aWJepd{q5&S!YBr>Mm{gz9Xmjiq8`U-RZQI zA4iUJbzxcPBetWeI`Rd!Zu_q?X}nXks=GbcRXRUdH>>t11zF#Y(Oys@Y}1cZaM9dN zB4t%%iE`6?)f%IG@aAGe1_2{&uFQSsgo7m&L^KAgC(**<YKR~j=&&dgC0SySIJGNqGUns{^EJxlzO4X%?F` z5HLi^E%W32w5noV#?sDbzIg+KwA$O*8)xBplc&Ba=$_f-$gaxC6L`tq9O`khSdEIK>WC6dZrW@@ePv*!#r zJ5Ws!dn1o1(tNB(cR_l^PZjaOw1}b%rch^(8vefC#EP>gsBc|UO-)W&8Pm<(J+Gjk zfau%)zMI3;JFT=|+Up5i8ep6XR_fDOfINF~WxF=X-=mvOH-2A4cdHt5Ty2@i!bQgS<#r7BF z!skszSfd(958@bu&uz!wl~F9bACiGJ0JhTev;3#8;V^YutPbpvsBAYo=q?L)`;VrA zEsIi*yIVf$qp*>D3ECKTOQc@j-t#al3f#C6{_2&DT;pnehLzdJE1{NR9hYD+%-$B? zz?iOf(Qhq^RxC0h$5T~ErYZGhuC$j1G`H_Wghv(P(K?l6W=bV9hPuu!%e}A#nN?A! z_jAux2$F`i#Mf>Ul?Y_1%W*6|pAjyQG+($CWgbiaMI#2NWML;`I{5 zrqW8jxE%hX&s*+f&Lm2GQ(|f652)k zH5+AWTVMD0^K*uf+B{=Qkx`QJU&>M4BQunX*-G+CCnfliWYgn=*Ts_sMp-W#vcz@+ zR1s5wBe%T5U`~>A4r)UP5jph#7%MTu`sm+6NI`?vKfLl4M8jy5?x5qCpE&ne=Ul#S z(|vuKGL3i8oI}(6X>cClKIV^=?T4gDZuI1?EOkAO0*z~(l{EGli+y&Ema{?6FQK7lm%C-xuOg`ID><$R8N?imnfl&; zadLC$;ArFo)77R`4VU(8BNK6}KSb|s5nvdcD{DU_XEyjA(fK{$C%0CFwJ}MT#g+(G z$sDM*{6kb(Wjqr2@;7(h@cWXiEOz=|V4yD5(f14uGwMylrI=MzRCInl%TYe+(A5$3 z)`WFR`XgnRiumt!qNC)1aM>z6Cd%3eLv$>xtjmAqufh@|iY6x6#aUnerg;Dw1?7)} zBN~O$JB-}`n$E(`9-0sm(;GNa(6*MiVAMc=VbJi{dTo6@&-X~QZVXfy)B{9Oey#~N zsa@~Ck~&`Ys{Cz(G?HK7~R~Hf+t#H zV2EberGo+ZJ^GSr5Qa`dQ-=unvJ5zv$rRNp_X=T~zGRRuXgtFmAv_>U1R6(*#i|LJhBt>Li=u+4Hkw*xUq zDk%sHxioai+x_rZ8ZdGDUG7Y|++#rq|5!DSXOFC~bvh!dv(MqBrm#R5n;-y-=UZc?-I_K4NRSAr#0!cLCeb()IL) zJti|VGcYNC&NT@)KZdQ;pUFn!EP!o+%ITx9_i!~;f|$Q{h51cbvHTv-Z|K%6Y=JLZ z4~yM!8J0$Y%8?K32iB|M?Dx?$zt5@tC#8+-8Sy{8^w_vK-RlFw9Hy`EAw+&j#n;sD zcir|E`M2fwdWG7Sj}Nv9weRP;7Tu!QS}1JpL7t-(I^GUk&HkdB;sV@D@TjbkSEcq( zUt9R~Bvi}2$>0^-X!>Avb%BPs>8ecOD^%;xc6+^uLcHUW!ain;b!T#cN)^OB*l)e) z8o%cszExDeT5q$uIb%dj%pEWa>mXfz?&QzyB-%H4(;k zbLo#s*%?>yT=if*avI;GO$D%N{Y1TqFxRhY1Mk5emyFLi25jcsIuCxg;m?X11O&@} z)+r?gr;v0J*S_>VfpAVocdV5oOU#$H9H#OnpAD)3p3>nGqzXQ@1bP>bmlA~=)_!Lp z&SE-JD|`1Qs6Llkf*Zxf{Js`o{TM4tt?)M|t~MNSva!M^h+m0{H%;6FkJjVULpUsj z?;*F9F8EMt^FH3j))qez#}!O`Sy(O|SJ4wZ{|>#v%>0&?mS4oYt>>5fCTgjFTIHwB z4i2@^ggpI2a_r-q=US8?cD%=*DRv@>j&(dA>#>2J2&Pr%SUZli26=(~YI94D%*Ag! zOlB3l#8M~v=S;+15TnNnjiACFvI}b$SU#VSY{&&_fzjqqX7eVOMYf*I`0)11ypE?I z-^ACxRR-p(z#Q`3?gptPVN`Vd74KRUU`5ro9x+uI=EYjDR_hWK6{ooSO7SJT8WOC& zgXgaPYoYtQU%BxqMbOS#&-m{?r|Dj1sq7?(?~(wABES|q;NqSt+~n>69TOWLpTs<` zy4p1uBOd+h_&7Oa8i;3?VcAK;V%~4bRm90U_8@H6)HjD#wl`cA0e+gzwl`^FdI^6tH`X=;_8=;a<)6F5RGUJypdY zqvsowM?2l0tfotV953ChFHm=@f=J>dgraSzwwphyz zeA;T2I67u4AC!USAI@M{qHBz8*NI6`$?MpJzzP>tAJ;k$pn?{NXD8#=};H?gIkKb$~64ZL`ge9v`fB}IW9wHYi-fU#d z9~SG=E!24Nz&;V$Fzr44M5y&qoK=g(27L!!M9%?_?`19RsoyNk4r#p9sH2^osuB5{ z-3QT3eM=ohC7XH!hKFm*WIw@+eJ+h|7Ka&oj}rfuTx>S2_aPyOq!g3DIv_{po9ztPU^b-+V1(ta62q)f3~G*0h^wTh=qu;f!f=B z>3Q-IF%?M====fNi1lOX_U@CiDvs6(FxNA0A{%#GJXsCULhms6t6-{1k*X!7EopTu z8q`c1ulyp`MR6V8B32kf57M0c-@IYB%4MmE0U?}Zhni?x;c<^!iR&FXiaidek7 z1KH<8tu40^tAkn5PBnAb4>mo1xUo6>q804+&sVJ(c(xI|N6ybA#iHuHesS(ez2BKn z@!(sWv6$b!=s=b@ZJEhK22Sr-go^}5X=1$2dUe__zh+Vq%wdc z&Z%xo5}zuAufIS?`h&?)4!=EQH!PriLYas zO*j9iXmYRPSG?jw1O1x5MlK>^x5BM2x5>v0$;63-Mx9okH8A0j5K=uuKJ&z}lGPR{ zJ`K0JuivW@ov=SjqCc?GzRViMLbnvH>%Y(}Uac4>;7jdj6_H?KB5oBJgD=5q0wx^; zU(v74|D|A}?6rC74G`zWvft`NU2=_QN|_IH+RQDdB*u5MCt@?|qKv$eIgVnvyMNJ_x5a7ilNB56G7ELPTMT3H%dRUF7V0=BSec0T~^CyqTmp=@eBRW>L+T$H&?uwT>PJ@D(dKk-7J2Q zyb!ZrpdiCzKVGWaW3VnkYZjJ5i-SkAFBOTX&6WneRlP4|I z@fi8|QDfTL;^9MFEv-)>ZbL4--Hi}L=yVRd3|!p=-=lMq=uoE`Uz|kde4Z=DY>)5SuHsa!BeN8#2DPLVipC={)}c|@ala86Dl|KJQ@C-_dM~3)H7?O#qwxHL z(4F9ts$@jY=*Id)^%1*^rU6UDE6wK#)ekP-D~jGpW-xAJ)QKzU6H1l61yOsqqrUwT zy=|9xD)D+9Ge*|OYn$0sh9XSNxAFPS8+n>7B;=oF#S!{1ER5szxczb(7g>!ClIkAH zzD{@$*Rw*_;rfa`wY2nSZaw>s?kHZ0te#M@&q1Q%Ist142fi$&wZsd(=9GW|o-#+B z23_mSr&O^+6N$cuxD}Z{WJzCel3IO9J{j+fRN4_wCy&1&Y;fWVQp;Z_A^)WR3aLeX z43_5jN2z6PFzVk`TFQ?;u=kg<;9^E7GX*D&@7!XHVj0w@SH0)sLlLoTR88agBuYYY z%bKZL-RcO;J!rCoapb;gO{AAmZVL{bi*{c?zq5ADdoN%mOE{N;gH_D7v^^icPoHx^ zA2Zc^LYE*-&2B&&(bi8?%xKh`@4mGDzl)%hvJ}IL~?TA@BJ)}=PoXf z`{vzKu(ZatDG~l--)hcj-i<9j4&x;A<|TdM_;W}mvR&YeMyh8%IM+t5oG0KsAG>p)|83*wEO>MUR%8(+Uv~R6K(eHD=u(h4p8W($dnlA|-#p z;^lUBb_;jF`lE+{y}q^MbR@_NY}yQ{2=V<9SdKCI*)x0zTrRo`rNr}mnHc2qKgLYv zrcHX||9ZeQ%Kx`x)GwqZCce_xd*ZL9pr9bdkU>Y^{kIKtQL{E_EYm;B(Cao0?ZGQ2 wg$A*N^!Y;S{=a3E|JH&2uNAWXtE9wZ8Qx1v--an1iZ8)GHKkiHPFjTgKe&z$*#H0l literal 0 HcmV?d00001 diff --git a/docs/ui/test_name.png b/docs/ui/test_name.png new file mode 100644 index 0000000000000000000000000000000000000000..3d18df19d031cfacd7e1befe745efc57be0109cc GIT binary patch literal 10221 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 G-G2dumwj~r literal 0 HcmV?d00001 diff --git a/framework/python/src/api/api.py b/framework/python/src/api/api.py index 6b89da795..1eff52525 100644 --- a/framework/python/src/api/api.py +++ b/framework/python/src/api/api.py @@ -28,9 +28,10 @@ DEVICE_MAC_ADDR_KEY = "mac_addr" DEVICE_MANUFACTURER_KEY = "manufacturer" DEVICE_MODEL_KEY = "model" +DEVICE_TEST_MODULES_KEY = "test_modules" class Api: - """Provide REST endpoints to manage Test Run""" + """Provide REST endpoints to manage Testrun""" def __init__(self, test_run): @@ -54,7 +55,7 @@ def __init__(self, test_run): self._router.add_api_route("/device", self.save_device, methods=["POST"]) # TODO: Make this configurable in system.json - origins = ["http://localhost:4200"] + origins = ["http://localhost:8080", "http://localhost:4200"] self._app = FastAPI() self._app.include_router(self._router) @@ -67,7 +68,7 @@ def __init__(self, test_run): ) self._api_thread = threading.Thread(target=self._start, - name="Test Run API", + name="Testrun API", daemon=True) def start(self): @@ -127,11 +128,14 @@ async def start_test_run(self, request: Request, response: Response): device = self._session.get_device(body_json["device"]["mac_addr"]) - # Check Test Run is not already running - if self._test_run.get_session().get_status() != "Idle": - LOGGER.debug("Test Run is already running. Cannot start another instance") + # Check Testrun is not already running + if self._test_run.get_session().get_status() in [ + "In Progress", + "Waiting for Device", + ]: + LOGGER.debug("Testrun is already running. Cannot start another instance") response.status_code = status.HTTP_409_CONFLICT - return self._generate_msg(False, "Test Run is already running") + return self._generate_msg(False, "Testrun is already running") # Check if requested device is known in the device repository if device is None: @@ -141,17 +145,17 @@ async def start_test_run(self, request: Request, response: Response): device.firmware = body_json["device"]["firmware"] - # Check Test Run is able to start + # Check Testrun is able to start if self._test_run.get_net_orc().check_config() is False: response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR return self._generate_msg(False,"Configured interfaces are not ready for use. Ensure required interfaces are connected.") self._test_run.get_session().reset() self._test_run.get_session().set_target_device(device) - LOGGER.info(f"Starting Test Run with device target {device.manufacturer} {device.model} with MAC address {device.mac_addr}") + LOGGER.info(f"Starting Testrun with device target {device.manufacturer} {device.model} with MAC address {device.mac_addr}") thread = threading.Thread(target=self._start_test_run, - name="Test Run") + name="Testrun") thread.start() return self._test_run.get_session().to_json() @@ -165,9 +169,9 @@ def _start_test_run(self): self._test_run.start() async def stop_test_run(self): - LOGGER.debug("Received stop command. Stopping Test Run") + LOGGER.debug("Received stop command. Stopping Testrun") self._test_run.stop() - return self._generate_msg(True, "Test Run stopped") + return self._generate_msg(True, "Testrun stopped") async def get_status(self): return self._test_run.get_session().to_json() @@ -193,10 +197,11 @@ async def save_device(self, request: Request, response: Response): # Create new device device = Device() - device.mac_addr = device_json.get(DEVICE_MAC_ADDR_KEY) + device.mac_addr = device_json.get(DEVICE_MAC_ADDR_KEY).lower() device.manufacturer = device_json.get(DEVICE_MANUFACTURER_KEY) device.model = device_json.get(DEVICE_MODEL_KEY) device.device_folder = device.manufacturer + " " + device.model + device.test_modules = device_json.get(DEVICE_TEST_MODULES_KEY) self._test_run.create_device(device) response.status_code = status.HTTP_201_CREATED @@ -214,9 +219,26 @@ async def save_device(self, request: Request, response: Response): return self._generate_msg(False, "Invalid JSON received") def _validate_device_json(self, json_obj): + + # Check all required properties are present if not (DEVICE_MAC_ADDR_KEY in json_obj and DEVICE_MANUFACTURER_KEY in json_obj and DEVICE_MODEL_KEY in json_obj ): return False + + # Check length of strings + if len(json_obj.get(DEVICE_MANUFACTURER_KEY)) > 64 or len( + json_obj.get(DEVICE_MODEL_KEY)) > 64: + return False + + disallowed_chars = ["/", "\\", "\'", "\"", ";"] + for char in json_obj.get(DEVICE_MANUFACTURER_KEY): + if char in disallowed_chars: + return False + + for char in json_obj.get(DEVICE_MODEL_KEY): + if char in disallowed_chars: + return False + return True diff --git a/framework/python/src/common/logger.py b/framework/python/src/common/logger.py index 8dd900fea..9bc8ecc04 100644 --- a/framework/python/src/common/logger.py +++ b/framework/python/src/common/logger.py @@ -24,14 +24,19 @@ _CONF_DIR = 'local' _CONF_FILE_NAME = 'system.json' + + # Set log level +log_level = _DEFAULT_LEVEL + try: with open(os.path.join(_CONF_DIR, _CONF_FILE_NAME), encoding='UTF-8') as config_json_file: system_conf_json = json.load(config_json_file) - log_level_str = system_conf_json['log_level'] - log_level = logging.getLevelName(log_level_str) + if 'log_level' in system_conf_json: + log_level_str = system_conf_json['log_level'] + log_level = logging.getLevelName(log_level_str) except OSError: # TODO: Print out warning that log level is incorrect or missing log_level = _DEFAULT_LEVEL diff --git a/framework/python/src/common/session.py b/framework/python/src/common/session.py index f8c8d04b5..638d213a8 100644 --- a/framework/python/src/common/session.py +++ b/framework/python/src/common/session.py @@ -17,6 +17,7 @@ import datetime import json import os +from common import util, logger NETWORK_KEY = 'network' DEVICE_INTF_KEY = 'device_intf' @@ -28,6 +29,8 @@ API_PORT_KEY = 'api_port' MAX_DEVICE_REPORTS_KEY = 'max_device_reports' +LOGGER = logger.get_logger('session') + class TestRunSession(): """Represents the current session of Test Run.""" @@ -45,7 +48,7 @@ def __init__(self, config_file): self._load_config() def start(self): - self._status = 'Waiting for device' + self._status = 'Waiting for Device' self._started = datetime.datetime.now() def get_started(self): @@ -76,27 +79,34 @@ def get_config(self): def _load_config(self): + LOGGER.debug(f'Loading configuration file at {self._config_file}') if not os.path.isfile(self._config_file): + LOGGER.error(f'No configuration file present at {self._config_file}. ' + + 'Default configuration will be used.') return with open(self._config_file, 'r', encoding='utf-8') as f: config_file_json = json.load(f) # Network interfaces - if (NETWORK_KEY in config_file_json + if (NETWORK_KEY in config_file_json and DEVICE_INTF_KEY in config_file_json.get(NETWORK_KEY) and INTERNET_INTF_KEY in config_file_json.get(NETWORK_KEY)): - self._config[NETWORK_KEY][DEVICE_INTF_KEY] = config_file_json.get(NETWORK_KEY, {}).get(DEVICE_INTF_KEY) - self._config[NETWORK_KEY][INTERNET_INTF_KEY] = config_file_json.get(NETWORK_KEY, {}).get(INTERNET_INTF_KEY) + self._config[NETWORK_KEY][DEVICE_INTF_KEY] = config_file_json.get( + NETWORK_KEY, {}).get(DEVICE_INTF_KEY) + self._config[NETWORK_KEY][INTERNET_INTF_KEY] = config_file_json.get( + NETWORK_KEY, {}).get(INTERNET_INTF_KEY) if RUNTIME_KEY in config_file_json: self._config[RUNTIME_KEY] = config_file_json.get(RUNTIME_KEY) if STARTUP_TIMEOUT_KEY in config_file_json: - self._config[STARTUP_TIMEOUT_KEY] = config_file_json.get(STARTUP_TIMEOUT_KEY) + self._config[STARTUP_TIMEOUT_KEY] = config_file_json.get( + STARTUP_TIMEOUT_KEY) if MONITOR_PERIOD_KEY in config_file_json: - self._config[MONITOR_PERIOD_KEY] = config_file_json.get(MONITOR_PERIOD_KEY) + self._config[MONITOR_PERIOD_KEY] = config_file_json.get( + MONITOR_PERIOD_KEY) if LOG_LEVEL_KEY in config_file_json: self._config[LOG_LEVEL_KEY] = config_file_json.get(LOG_LEVEL_KEY) @@ -105,11 +115,15 @@ def _load_config(self): self._config[API_PORT_KEY] = config_file_json.get(API_PORT_KEY) if MAX_DEVICE_REPORTS_KEY in config_file_json: - self._config[MAX_DEVICE_REPORTS_KEY] = config_file_json.get(MAX_DEVICE_REPORTS_KEY) + self._config[MAX_DEVICE_REPORTS_KEY] = config_file_json.get( + MAX_DEVICE_REPORTS_KEY) + + LOGGER.debug(self._config) def _save_config(self): with open(self._config_file, 'w', encoding='utf-8') as f: f.write(json.dumps(self._config, indent=2)) + util.set_file_owner(owner=util.get_host_user(), path=self._config_file) def get_runtime(self): return self._config.get(RUNTIME_KEY) @@ -142,7 +156,7 @@ def get_max_device_reports(self): return self._config.get(MAX_DEVICE_REPORTS_KEY) def set_config(self, config_json): - self._config = config_json + self._config.update(config_json) self._save_config() def set_target_device(self, device): @@ -192,8 +206,7 @@ def get_all_reports(self): device_reports = device.get_reports() for device_report in device_reports: reports.append(device_report.to_json()) - - return reports + return sorted(reports, key=lambda report: report['started'], reverse=True) def add_total_tests(self, no_tests): self._total_tests += no_tests diff --git a/framework/python/src/common/testreport.py b/framework/python/src/common/testreport.py index ba35ff27a..02c9d65a9 100644 --- a/framework/python/src/common/testreport.py +++ b/framework/python/src/common/testreport.py @@ -1,84 +1,559 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -"""Store previous test run information.""" - -from datetime import datetime - -DATE_TIME_FORMAT = '%Y-%m-%d %H:%M:%S' - -class TestReport(): - """Represents a previous Test Run report.""" - - def __init__(self, - status='Non-Compliant', - started=None, - finished=None, - total_tests=0 - ): - self._device = {} - self._status: str = status - self._started = started - self._finished = finished - self._total_tests = total_tests - self._results = [] - - def get_status(self): - return self._status - - def get_started(self): - return self._started - - def get_finished(self): - return self._finished - - def get_duration_seconds(self): - diff = self._finished - self._started - return diff.total_seconds() - - def get_duration(self): - return str(datetime.timedelta(seconds=self.get_duration_seconds())) - - def add_test(self, test): - self._results.append(test) - - def to_json(self): - report_json = {} - report_json['device'] = self._device - report_json['status'] = self._status - report_json['started'] = self._started.strftime(DATE_TIME_FORMAT) - report_json['finished'] = self._finished.strftime(DATE_TIME_FORMAT) - report_json['tests'] = {'total': self._total_tests, - 'results': self._results} - return report_json - - def from_json(self, json_file): - - self._device['mac_addr'] = json_file['device']['mac_addr'] - self._device['manufacturer'] = json_file['device']['manufacturer'] - self._device['model'] = json_file['device']['model'] - - if 'firmware' in self._device: - self._device['firmware'] = json_file['device']['firmware'] - - self._status = json_file['status'] - self._started = datetime.strptime(json_file['started'], DATE_TIME_FORMAT) - self._finished = datetime.strptime(json_file['finished'], DATE_TIME_FORMAT) - self._total_tests = json_file['tests']['total'] - - # Loop through test results - for test_result in json_file['tests']['results']: - self.add_test(test_result) - - return self +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Store previous test run information.""" + +from datetime import datetime +from weasyprint import HTML +from io import BytesIO +import base64 +import os + +DATE_TIME_FORMAT = '%Y-%m-%d %H:%M:%S' +RESOURCES_DIR = 'resources/report' + +# Locate parent directory +current_dir = os.path.dirname(os.path.realpath(__file__)) + +# Locate the test-run root directory, 4 levels, src->python->framework->test-run +root_dir = os.path.dirname(os.path.dirname( + os.path.dirname(os.path.dirname(current_dir)))) + +# Obtain the report resources directory +report_resource_dir = os.path.join(root_dir, + RESOURCES_DIR) + +font_file = os.path.join(report_resource_dir,'GoogleSans-Regular.ttf') +test_run_img_file = os.path.join(report_resource_dir,'testrun.png') + +class TestReport(): + """Represents a previous Test Run report.""" + + def __init__(self, + status='Non-Compliant', + started=None, + finished=None, + total_tests=0 + ): + self._device = {} + self._status: str = status + self._started = started + self._finished = finished + self._total_tests = total_tests + self._results = [] + + def get_status(self): + return self._status + + def get_started(self): + return self._started + + def get_finished(self): + return self._finished + + def get_duration_seconds(self): + diff = self._finished - self._started + return diff.total_seconds() + + def get_duration(self): + return str(datetime.timedelta(seconds=self.get_duration_seconds())) + + def add_test(self, test): + self._results.append(test) + + def to_json(self): + report_json = {} + report_json['device'] = self._device + report_json['status'] = self._status + report_json['started'] = self._started.strftime(DATE_TIME_FORMAT) + report_json['finished'] = self._finished.strftime(DATE_TIME_FORMAT) + report_json['tests'] = {'total': self._total_tests, + 'results': self._results} + return report_json + + def from_json(self, json_file): + + self._device['mac_addr'] = json_file['device']['mac_addr'] + self._device['manufacturer'] = json_file['device']['manufacturer'] + self._device['model'] = json_file['device']['model'] + + if 'firmware' in self._device: + self._device['firmware'] = json_file['device']['firmware'] + + self._status = json_file['status'] + self._started = datetime.strptime(json_file['started'], DATE_TIME_FORMAT) + self._finished = datetime.strptime(json_file['finished'], DATE_TIME_FORMAT) + self._total_tests = json_file['tests']['total'] + + # Loop through test results + for test_result in json_file['tests']['results']: + self.add_test(test_result) + + return self + + # Create a pdf file in memory and return the bytes + def to_pdf(self): + # Resolve the data as html first + report_html = self.to_html() + + # Convert HTML to PDF in memory using weasyprint + pdf_bytes = BytesIO() + HTML(string=report_html).write_pdf(pdf_bytes) + return pdf_bytes + + def to_html(self): + json_data = self.to_json() + return f''' + + + {self.generate_head()} + + {self.generate_body(json_data)} + + + ''' + + def generate_test_sections(self,json_data): + results = json_data["tests"]["results"] + sections = "" + for result in results: + sections += self.generate_test_section(result) + return sections + + def generate_test_section(self, result): + section_content = '

\n' + for key, value in result.items(): + if value is not None: # Check if the value is not None + formatted_key = key.replace('_', ' ').title() # Replace underscores and capitalize + section_content += f'

{formatted_key}: {value}

\n' + section_content += '
\n
\n' + return section_content + + def generate_pages(self,json_data): + max_page = 1 + reports_per_page = 25 # figure out how many can fit on other pages + + # Calculate pages + test_count = len(json_data['tests']['results']) + + # 10 tests can fit on the first page + if test_count > 10: + test_count -= 10 + + full_page = (int)(test_count / reports_per_page) + partial_page = 1 if test_count % reports_per_page > 0 else 0 + if partial_page > 0: + max_page += full_page + partial_page + + pages = '' + for i in range(max_page): + pages += self.generate_page(json_data, i+1, max_page) + return pages + + def generate_page(self,json_data, page_num, max_page): + version = 'v1.0 (2023-10-02)' # Place holder until available in json report + page = '
' + page += self.generate_header(json_data) + if page_num == 1: + page += self.generate_summary(json_data) + page += self.generate_results(json_data, page_num) + page += self.generate_footer(page_num,max_page,version) + page += '
' + if page_num < max_page: + page += '
' + #page += f'''

''' + return page + + def generate_body(self,json_data, page_num=1, max_page=1): + return f''' + + {self.generate_pages(json_data)} + + ''' + + def generate_footer(self,page_num, max_page, version): + footer = f''' + + ''' + return footer + + def generate_results(self,json_data, page_num): + + result_list = ''' + +
+ Results List +
+
Name
+
Description
+
Result
+
''' + if page_num == 1: + start = 0 + else: + start = 10 * (page_num - 1) + (page_num-2) * 25 + results_on_page = 10 if page_num == 1 else 25 + result_end = min(results_on_page,len(json_data['tests']['results'])) + for ix in range(result_end-start): + result = json_data['tests']['results'][ix+start] + result_list += self.generate_result(result) + result_list += '
' + return result_list + + def generate_result(self,result): + if result['result'] == 'Non-Compliant': + result_class = 'result-test-result-non-compliant' + elif result['result'] == 'Compliant': + result_class = 'result-test-result-compliant' + else: + result_class = 'result-test-result-skipped' + + result_html = f''' +
+
{result['name']}
+
{result['test_description']}
+
{result['result']}
+
+ ''' + return result_html + + def generate_header(self, json_data): + with open(test_run_img_file, 'rb') as f: + tr_img_b64 = base64.b64encode(f.read()).decode('utf-8') + return f''' +
+

Testrun report

+

{json_data["device"]["manufacturer"]} {json_data["device"]["model"]}

+ Test Run +
+ ''' + + def generate_summary(self, json_data): + # Generate the basic content section layout + summary = ''' +
+ +
+ ''' + # Add the device information + manufacturer = json_data['device']['manufacturer'] if 'manufacturer' in json_data['device'] else 'Undefined' + model = json_data['device']['model'] if 'model' in json_data['device'] else 'Undefined' + fw = json_data['device']['firmware'] if 'firmware' in json_data['device'] else 'Undefined' + mac = json_data['device']['mac_addr'] if 'mac_addr' in json_data['device'] else 'Undefined' + + summary += self.generate_device_summary_label('Manufacturer',manufacturer) + summary += self.generate_device_summary_label('Model',model) + summary += self.generate_device_summary_label('Firmware',fw) + summary += self.generate_device_summary_label('MAC Address',mac,trailing_space=False) + + # Add the result summary + summary += self.generate_result_summary(json_data) + + summary += '\n
' + return summary + + def generate_result_summary(self,json_data): + if json_data['status'] == 'Compliant': + result_summary = '''
''' + else: + result_summary = '''
''' + result_summary += self.generate_result_summary_item('Test status', 'Complete') + result_summary += self.generate_result_summary_item('Test result', json_data['status'], style='color: white; font-size:24px; font-weight: 700;') + result_summary += self.generate_result_summary_item('Started', json_data['started']) + + # Convert the timestamp strings to datetime objects + start_time = datetime.strptime(json_data['started'], "%Y-%m-%d %H:%M:%S") + end_time = datetime.strptime(json_data['finished'], "%Y-%m-%d %H:%M:%S") + # Calculate the duration + duration = end_time - start_time + result_summary += self.generate_result_summary_item('Duration',str(duration)) + + result_summary += '\n
' + return result_summary + + def generate_result_summary_item(self, key, value, style=None): + summary_item = f'''
{key}
''' + if style is not None: + summary_item += f'''
{value}
''' + else: + summary_item += f'''
{value}
''' + return summary_item + + def generate_device_summary_label(self, key, value, trailing_space=True): + label = f''' +
{key}
+
{value}
+ ''' + if trailing_space: + label += '''
''' + return label + + def generate_head(self): + return f''' + + + + Testrun Report + + + ''' + + def generate_css(self): + return ''' + /* Set some global variables */ + :root { + --header-height: .75in; + --header-width: 8.5in; + --header-pos-x: 0in; + --header-pos-y: 0in; + --summary-width: 8.5in; + --summary-height: 2.8in; + --vertical-line-height: calc(var(--summary-height)-.2in); + --vertical-line-pos-x: 25%; + } + + @font-face { + font-family: 'Google Sans'; + font-style: normal; + src: url(https://fonts.gstatic.com/s/googlesans/v58/4Ua_rENHsxJlGDuGo1OIlJfC6l_24rlCK1Yo_Iqcsih3SAyH6cAwhX9RFD48TE63OOYKtrwEIJllpyk.woff2) format('woff2'); + unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD; + } + + /* Define some common body formatting*/ + body { + font-family: 'Google Sans', sans-serif; + margin: 0px; + padding: 0px; + } + + /* Use this for various section breaks*/ + .gradient-line { + position: relative; + background-image: linear-gradient(to right, red, blue, green, yellow, orange); + height: 1px; + /* Adjust the height as needed */ + width: 100%; + /* To span the entire width */ + display: block; + /* Ensures it's a block-level element */ + } + + /* Sets proper page size during print to pdf for weasyprint */ + @page { + size: Letter; + width: 8.5in; + height: 11in; + } + + .page { + position: relative; + margin: 0 20px; + width: 8.5in; + height: 11in; + } + + /* Define the header related css elements*/ + .header { + position: relative; + } + + .header-text { + margin: 0 0 8px 0; + font-size: 20px; + font-weight: 400; + } + + .header-title { + margin: 0px; + font-size: 48px; + font-weight: 700; + } + + /* Define the summary related css elements*/ + .summary-content { + position: relative; + width: var(--summary-width); + height: var(--summary-height); + margin-top: 19px; + margin-bottom: 19px; + } + + .summary-item-label { + position: relative; + font-size: 12px; + font-weight: 500; + color: #5F6368; + } + + .summary-item-value { + position: relative; + font-size: 20px; + font-weight: 400; + color: #202124; + } + + .summary-item-space { + position: relative; + padding-bottom: 15px; + margin: 0; + } + + .summary-vertical-line { + width: 1px; + height: var(--vertical-line-height); + background-color: #80868B; + position: absolute; + top: .3in; + bottom: .1in; + left: 3in; + } + + /* CSS for the color box */ + .summary-color-box { + position: absolute; + right: 0in; + top: .3in; + width: 2.6in; + height: 226px; + } + + .summary-box-compliant { + background-color: rgb(24, 128, 56); + } + + .summary-box-non-compliant { + background-color: #b31412; + } + + .summary-box-label { + font-size: 14px; + margin-top: 5px; + color: #DADCE0; + position: relative; + top: 10px; + left: 10px; + font-weight: 500; + } + + .summary-box-value { + font-size: 18px; + margin: 0 0 10px 0; + color: #ffffff; + position: relative; + top: 10px; + left: 10px; + } + + .result-list-title { + font-size: 24px; + } + + .result-list { + position: relative; + margin-top: .2in; + font-size: 18px; + } + + .result-line { + border: 1px solid #D3D3D3; + /* Light Gray border*/ + height: .4in; + width: 8.5in; + } + + .result-line-result { + border-top: 0px; + } + + .result-list-header-label { + font-weight: 500; + position: absolute; + font-size: 12px; + font-weight: bold; + height: 40px; + display: flex; + align-items: center; + } + + .result-test-label { + position: absolute; + font-size: 12px; + margin-top: 12px; + max-width: 300px; + font-weight: normal; + align-items: center; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + } + + .result-test-description { + max-width: 380px; + } + + .result-test-result-non-compliant { + background-color: #FCE8E6; + color: #C5221F; + left: 7.02in; + } + + .result-test-result { + position: absolute; + font-size: 12px; + width: fit-content; + height: 12px; + margin-top: 8px; + padding: 4px 4px 7px 5px; + border-radius: 2px; + } + + .result-test-result-compliant { + background-color: #E6F4EA; + color: #137333; + left: 7.16in; + } + + .result-test-result-skipped { + background-color: #e3e3e3; + color: #393939; + left: 7.2in; + } + + /* CSS for the footer */ + .footer { + position: absolute; + height: 30px; + width: 8.5in; + bottom: 0in; + } + + .footer-label { + position: absolute; + top: 20px; + font-size: 12px; + } + + @media print { + @page { + size: Letter; + width: 8.5in; + height: 11in; + } + }''' \ No newline at end of file diff --git a/framework/python/src/common/util.py b/framework/python/src/common/util.py index 441b93224..3916ce141 100644 --- a/framework/python/src/common/util.py +++ b/framework/python/src/common/util.py @@ -93,3 +93,6 @@ def get_user(): else: LOGGER.error('An exception occurred:', e) return user + +def set_file_owner(path, owner): + run_command(f'chown -R {owner} {path}') diff --git a/framework/python/src/core/test_runner.py b/framework/python/src/core/test_runner.py index 9962c3995..f3db52eec 100644 --- a/framework/python/src/core/test_runner.py +++ b/framework/python/src/core/test_runner.py @@ -34,7 +34,7 @@ class TestRunner: def __init__(self, config_file=None, - validate=True, + validate=False, net_only=False, single_intf=False, no_ui=False): @@ -76,9 +76,10 @@ def parse_args(): help="Define the configuration file for Test Run and Network Orchestrator" ) parser.add_argument( - "--no-validate", + "--validate", + default=False, action="store_true", - help="Turn off the validation of the network after network boot") + help="Turn on the validation of the network after network boot") parser.add_argument("-net", "--net-only", action="store_true", @@ -97,7 +98,7 @@ def parse_args(): if __name__ == "__main__": args = parse_args() runner = TestRunner(config_file=args.config_file, - validate=not args.no_validate, + validate=args.validate, net_only=args.net_only, single_intf=args.single_intf, no_ui=args.no_ui) diff --git a/framework/python/src/core/testrun.py b/framework/python/src/core/testrun.py index 9034f5796..d66f599e3 100644 --- a/framework/python/src/core/testrun.py +++ b/framework/python/src/core/testrun.py @@ -20,6 +20,7 @@ Run using the provided command scripts in the cmd folder. E.g sudo cmd/start """ +import docker import json import os import sys @@ -65,7 +66,7 @@ class TestRun: # pylint: disable=too-few-public-methods def __init__(self, config_file, - validate=True, + validate=False, net_only=False, single_intf=False, no_ui=False): @@ -90,8 +91,8 @@ def __init__(self, self._session.add_runtime_param('single_intf') if net_only: self._session.add_runtime_param('net_only') - if not validate: - self._session.add_runtime_param('no-validate') + if validate: + self._session.add_runtime_param('validate') self.load_all_devices() @@ -113,10 +114,11 @@ def __init__(self, else: - # Build UI image + # Start UI container + self.start_ui() + self._api = Api(self) self._api.start() - # Start UI container # Hold until API ends while True: @@ -254,14 +256,14 @@ def save_device(self, device: Device, device_json): def start(self): - self._session.start() + self.get_session().start() self._start_network() if self._net_only: LOGGER.info('Network only option configured, no tests will be run') - self.get_net_orc().listener.register_callback( + self.get_net_orc().get_listener().register_callback( self._device_discovered, [NetworkEvent.DEVICE_DISCOVERED] ) @@ -286,10 +288,10 @@ def start(self): ) self.get_net_orc().start_listener() - self._set_status('Waiting for device') + self._set_status('Waiting for Device') LOGGER.info('Waiting for devices on the network...') - time.sleep(self._session.get_runtime()) + time.sleep(self.get_session().get_runtime()) if not (self._test_orc.test_in_progress() or self.get_net_orc().monitor_in_progress()): @@ -310,6 +312,7 @@ def stop(self, kill=False): self._stop_tests() self._stop_network(kill=kill) + self._stop_ui() def _register_exits(self): signal.signal(signal.SIGINT, self._exit_handler) @@ -352,7 +355,7 @@ def _stop_tests(self): def get_device(self, mac_addr): """Returns a loaded device object from the device mac address.""" - for device in self._session.get_device_repository(): + for device in self.get_session().get_device_repository(): if device.mac_addr == mac_addr: return device return None @@ -372,17 +375,50 @@ def _device_discovered(self, mac_addr): self.get_session().set_target_device(device) + self._set_status('In Progress') + LOGGER.info( - f'Discovered {device.manufacturer} {device.model} on the network. Waiting for device to obtain IP') + f'Discovered {device.manufacturer} {device.model} on the network. ' + + 'Waiting for device to obtain IP') def _device_stable(self, mac_addr): LOGGER.info(f'Device with mac address {mac_addr} is ready for testing.') - self._set_status('In progress') - self._test_orc.run_test_modules() - self._set_status('Complete') - - def _set_status(self, status): - self._session.set_status(status) + result = self._test_orc.run_test_modules() + self._set_status(result) def get_session(self): return self._session + + def _set_status(self, status): + self.get_session().set_status(status) + + def start_ui(self): + + self._stop_ui() + + LOGGER.info('Starting UI') + + client = docker.from_env() + + client.containers.run( + image='test-run/ui', + auto_remove=True, + name='tr-ui', + hostname='testrun.io', + detach=True, + ports={ + '80': 8080 + } + ) + + # TODO: Make port configurable + LOGGER.info('User interface is ready on http://localhost:8080') + + def _stop_ui(self): + client = docker.from_env() + try: + container = client.containers.get('tr-ui') + if container is not None: + container.kill() + except docker.errors.NotFound: + return diff --git a/framework/python/src/net_orc/network_orchestrator.py b/framework/python/src/net_orc/network_orchestrator.py index 4abdb9651..05733dfe0 100644 --- a/framework/python/src/net_orc/network_orchestrator.py +++ b/framework/python/src/net_orc/network_orchestrator.py @@ -114,12 +114,12 @@ def start_network(self): """Start the virtual testing network.""" LOGGER.info('Starting network') - self.build_network_modules() + #self.build_network_modules() self.create_net() self.start_network_services() - if 'no-validate' not in self._session.get_runtime_params(): + if 'validate' in self._session.get_runtime_params(): # Start the validator after network is ready self.validator.start() @@ -130,6 +130,7 @@ def get_listener(self): return self._listener def start_listener(self): + LOGGER.debug("Starting network listener") self.get_listener().start_listener() def stop(self, kill=False): @@ -272,7 +273,7 @@ def _ci_pre_network_create(self): """ Stores network properties to restore network after network creation and flushes internet interface """ - + LOGGER.info('Pre network create') self._ethmac = subprocess.check_output( f'cat /sys/class/net/{self._session.get_internet_interface()}/address', shell=True).decode('utf-8').strip() @@ -294,7 +295,7 @@ def _ci_pre_network_create(self): def _ci_post_network_create(self): """ Restore network connection in CI environment """ - LOGGER.info('post cr') + LOGGER.info('Post network create') util.run_command(((f'ip address del {self._ipv4} ' + 'dev {self._session.get_internet_interface()}'))) util.run_command((f'ip -6 address del {self._ipv6} ' + @@ -320,7 +321,7 @@ def _ci_post_network_create(self): def create_net(self): LOGGER.info('Creating baseline network') - if os.getenv('GITHUB_ACTIONS'): + if 'CI' in os.environ: self._ci_pre_network_create() # Setup the virtual network @@ -329,7 +330,7 @@ def create_net(self): self.stop() sys.exit(1) - if os.getenv("GITHUB_ACTIONS"): + if 'CI' in os.environ: self._ci_post_network_create() self._create_private_net() @@ -447,7 +448,7 @@ def _get_network_module(self, name): def _start_network_service(self, net_module): - LOGGER.debug('Starting net service ' + net_module.display_name) + LOGGER.debug('Starting network service ' + net_module.display_name) network = 'host' if net_module.net_config.host else PRIVATE_DOCKER_NET LOGGER.debug(f"""Network: {network}, image name: {net_module.image_name}, container name: {net_module.container_name}""") @@ -472,7 +473,7 @@ def _start_network_service(self, net_module): self._attach_service_to_network(net_module) def _stop_service_module(self, net_module, kill=False): - LOGGER.debug('Stopping Service container ' + net_module.container_name) + LOGGER.debug('Stopping network container ' + net_module.container_name) try: container = self._get_service_container(net_module) if container is not None: @@ -656,8 +657,7 @@ def restore_net(self): LOGGER.info('Clearing baseline network') - if hasattr(self, 'listener') and self.get_listener( - ) is not None and self.get_listener().is_running(): + if self.get_listener() is not None and self.get_listener().is_running(): self.get_listener().stop_listener() client = docker.from_env() diff --git a/framework/python/src/net_orc/network_validator.py b/framework/python/src/net_orc/network_validator.py index 2a4112764..3866bd3ae 100644 --- a/framework/python/src/net_orc/network_validator.py +++ b/framework/python/src/net_orc/network_validator.py @@ -56,7 +56,7 @@ def start(self): util.run_command(f'chown -R {host_user} {OUTPUT_DIR}') self._load_devices() - self._build_network_devices() + #self._build_network_devices() self._start_network_devices() def stop(self, kill=False): diff --git a/framework/python/src/test_orc/module.py b/framework/python/src/test_orc/module.py index 6f3c544a1..0cf0286d7 100644 --- a/framework/python/src/test_orc/module.py +++ b/framework/python/src/test_orc/module.py @@ -24,6 +24,7 @@ class TestModule: # pylint: disable=too-few-public-methods,too-many-instance-at name: str = None display_name: str = None description: str = None + enabled: bool = True tests: list = field(default_factory=lambda: []) # Docker settings diff --git a/framework/python/src/test_orc/test_orchestrator.py b/framework/python/src/test_orc/test_orchestrator.py index eb5676e17..6ab246b5c 100644 --- a/framework/python/src/test_orc/test_orchestrator.py +++ b/framework/python/src/test_orc/test_orchestrator.py @@ -33,6 +33,7 @@ LOG_REGEX = r"^[A-Z][a-z]{2} [0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} test_" SAVED_DEVICE_REPORTS = "local/devices/{device_folder}/reports" DEVICE_ROOT_CERTS = "local/root_certs" +TESTRUN_DIR = "/usr/local/testrun" class TestOrchestrator: @@ -65,7 +66,6 @@ def start(self): os.makedirs(DEVICE_ROOT_CERTS, exist_ok=True) self._load_test_modules() - self.build_test_modules() def stop(self): """Stop any running tests""" @@ -78,14 +78,30 @@ def run_test_modules(self): self._test_in_progress = True LOGGER.info( f"Running test modules on device with mac addr {device.mac_addr}") + + test_modules = [] for module in self._test_modules: + + if module is None or not module.enable_container or not module.enabled: + continue + + if not self._is_module_enabled(module, device): + continue + + test_modules.append(module) + self.get_session().add_total_tests(len(module.tests)) + + for module in test_modules: self._run_test_module(module) + LOGGER.info("All tests complete") self._session.stop() + report = TestReport().from_json(self._generate_report()) device.add_report(report) + self._write_reports(report) self._test_in_progress = False self._timestamp_results(device) @@ -95,33 +111,73 @@ def run_test_modules(self): LOGGER.debug("Old test results cleaned") self._test_in_progress = False + return report.get_status() + + def _write_reports(self, test_report): + + out_dir = os.path.join( + self._root_path, RUNTIME_DIR, + self._session.get_target_device().mac_addr.replace(":", "")) + + # Write the json report + with open(os.path.join(out_dir,"report.json"),"w", encoding="utf-8") as f: + json.dump(test_report.to_json(), f, indent=2) + + # Write the html report + with open(os.path.join(out_dir,"report.html"),"w", encoding="utf-8") as f: + f.write(test_report.to_html()) + + # Write the pdf report + with open(os.path.join(out_dir,"report.pdf"),"wb") as f: + f.write(test_report.to_pdf().getvalue()) + + util.run_command(f"chown -R {self._host_user} {out_dir}") + def _generate_report(self): report = {} - report["device"] = self._session.get_target_device().to_dict() - report["started"] = self._session.get_started().strftime( + report["device"] = self.get_session().get_target_device().to_dict() + report["started"] = self.get_session().get_started().strftime( "%Y-%m-%d %H:%M:%S") - report["finished"] = self._session.get_finished().strftime( + report["finished"] = self.get_session().get_finished().strftime( "%Y-%m-%d %H:%M:%S") report["status"] = self._calculate_result() - report["tests"] = self._session.get_report_tests() + report["tests"] = self.get_session().get_report_tests() + report["report"] = "file://" + os.path.join( + TESTRUN_DIR, + SAVED_DEVICE_REPORTS.replace( + "{device_folder}", + self.get_session().get_target_device().device_folder), + self.get_session().get_finished().strftime( + "%Y-%m-%dT%H:%M:%S"), + "report.pdf" + ) + out_file = os.path.join( self._root_path, RUNTIME_DIR, self._session.get_target_device().mac_addr.replace(":", ""), "report.json") + LOGGER.debug(f"Saving report to {out_file}") + + # Write report to runtime directory with open(out_file, "w", encoding="utf-8") as f: json.dump(report, f, indent=2) util.run_command(f"chown -R {self._host_user} {out_file}") + return report def _calculate_result(self): result = "Compliant" for test_result in self._session.get_test_results(): test_case = self.get_test_case(test_result["name"]) + if test_case is None: + LOGGER.error("Error occured whilst loading information about " + + f"test {test_result['name']}") + continue if (test_case.required_result.lower() == "required" and test_result["result"].lower() == "non-compliant"): - result = "non-compliant" + result = "Non-Compliant" return result def _cleanup_old_test_results(self, device): @@ -173,7 +229,7 @@ def _timestamp_results(self, device): device.mac_addr.replace(":", "")) # Define the destination results directory with timestamp - cur_time = datetime.now().strftime("%Y-%m-%dT%H:%M:%S") + cur_time = self.get_session().get_finished().strftime("%Y-%m-%dT%H:%M:%S") completed_results_dir = os.path.join( SAVED_DEVICE_REPORTS.replace("{device_folder}", device.device_folder), cur_time) @@ -201,12 +257,6 @@ def _run_test_module(self, module): device = self._session.get_target_device() - if module is None or not module.enable_container: - return - - if not self._is_module_enabled(module, device): - return - LOGGER.info("Running test module " + module.name) try: @@ -214,6 +264,9 @@ def _run_test_module(self, module): device_test_dir = os.path.join(self._root_path, RUNTIME_DIR, device.mac_addr.replace(":", "")) + root_certs_dir = os.path.join(self._root_path,DEVICE_ROOT_CERTS) + + container_runtime_dir = os.path.join(device_test_dir, module.name) os.makedirs(container_runtime_dir, exist_ok=True) @@ -251,10 +304,15 @@ def _run_test_module(self, module): source=device_monitor_capture, type="bind", read_only=True), + Mount(target="/testrun/root_certs", + source=root_certs_dir, + type="bind", + read_only=True) ], environment={ "HOST_USER": self._host_user, "DEVICE_MAC": device.mac_addr, + "IPV4_ADDR": device.ip_addr, "DEVICE_TEST_MODULES": json.dumps(device.test_modules), "IPV4_SUBNET": self._net_orc.network_config.ipv4_network, "IPV6_SUBNET": self._net_orc.network_config.ipv6_network @@ -276,7 +334,7 @@ def _run_test_module(self, module): log_stream = module.container.logs(stream=True, stdout=True, stderr=True) while (time.time() < test_module_timeout and status == "running" - and self._session.get_status() == "In progress"): + and self._session.get_status() == "In Progress"): try: line = next(log_stream).decode("utf-8").strip() if re.search(LOG_REGEX, line): @@ -296,15 +354,14 @@ def _run_test_module(self, module): module_results = module_results_json["results"] for test_result in module_results: self._session.add_test_result(test_result) + self._session.add_total_tests(1) except (FileNotFoundError, PermissionError, json.JSONDecodeError) as results_error: LOGGER.error( f"Error occured whilst obbtaining results for module {module.name}") LOGGER.debug(results_error) - self._session.add_total_tests(module.total_tests) - - LOGGER.info("Test module " + module.name + " has finished") + LOGGER.info(f"Test module {module.name} has finished") def _get_module_status(self, module): container = self._get_module_container(module) @@ -363,6 +420,10 @@ def _load_test_module(self, module_dir): module.name = module_json["config"]["meta"]["name"] module.display_name = module_json["config"]["meta"]["display_name"] module.description = module_json["config"]["meta"]["description"] + + if "enabled" in module_json["config"]: + module.enabled = module_json["config"]["enabled"] + module.dir = os.path.join(self._path, modules_dir, module_dir) module.dir_name = module_dir module.build_file = module_dir + ".Dockerfile" @@ -376,13 +437,13 @@ def _load_test_module(self, module_dir): try: test_case = TestCase( name=test_case_json["name"], - description=test_case_json["description"], + description=test_case_json["test_description"], expected_behavior=test_case_json["expected_behavior"], required_result=test_case_json["required_result"] ) module.tests.append(test_case) except Exception as error: - LOGGER.debug("Failed to load test case. See error for details") + LOGGER.error("Failed to load test case. See error for details") LOGGER.error(error) if "timeout" in module_json["config"]["docker"]: @@ -451,7 +512,7 @@ def _stop_module(self, module, kill=False): def get_test_modules(self): return self._test_modules - + def get_test_module(self, name): for test_module in self.get_test_modules(): if test_module.name == name: @@ -470,3 +531,6 @@ def get_test_case(self, name): if test_case.name == name: return test_case return None + + def get_session(self): + return self._session diff --git a/framework/requirements.txt b/framework/requirements.txt index 560c2baf9..3ce048ba3 100644 --- a/framework/requirements.txt +++ b/framework/requirements.txt @@ -7,8 +7,15 @@ ipaddress netifaces scapy +# Requirments for the test_orc module +weasyprint + # Requirements for the API fastapi==0.99.1 psutil uvicorn -pydantic==1.10.11 \ No newline at end of file +pydantic==1.10.11 + +# Requirements for testing +pytest +pytest-timeout diff --git a/local/system.json.example b/local/system.json.example index 17e5b0891..c640669b4 100644 --- a/local/system.json.example +++ b/local/system.json.example @@ -6,6 +6,6 @@ "log_level": "INFO", "startup_timeout": 60, "monitor_period": 300, - "runtime": 1200, + "runtime": 120, "max_device_reports": 5 -} \ No newline at end of file +} diff --git a/make/.gitignore b/make/.gitignore new file mode 100644 index 000000000..d93bca69e --- /dev/null +++ b/make/.gitignore @@ -0,0 +1,3 @@ +usr/ +bin/ +DEBIAN/postinst \ No newline at end of file diff --git a/make/DEBIAN/control b/make/DEBIAN/control new file mode 100644 index 000000000..9ad0ed2de --- /dev/null +++ b/make/DEBIAN/control @@ -0,0 +1,8 @@ +Package: Testrun +Version: 1.0 +Architecture: amd64 +Maintainer: Google +Homepage: https://github.com/google/testrun +Bugs: https://github.com/google/testrun/issues +Description: Automatically verify IoT device network behavior +Depends: libpangocairo-1.0-0, openvswitch-common, openvswitch-switch, build-essential, python3, python3-dev, python3-venv, net-tools diff --git a/make/DEBIAN/postinst b/make/DEBIAN/postinst new file mode 100755 index 000000000..0b6ac92de --- /dev/null +++ b/make/DEBIAN/postinst @@ -0,0 +1,36 @@ +#!/bin/bash -e + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +echo Installing application dependencies + +TESTRUN_DIR=/usr/local/testrun +cd $TESTRUN_DIR + +python3 -m venv venv + +source venv/bin/activate + +pip3 install -r framework/requirements.txt + +# Copy the default configuration +cp -n local/system.json.example local/system.json + +deactivate + +# Build docker images +sudo cmd/build + +echo Finished installing Testrun diff --git a/modules/network/dhcp-1/dhcp-1.Dockerfile b/modules/network/dhcp-1/dhcp-1.Dockerfile index 6b941d878..8ef896f17 100644 --- a/modules/network/dhcp-1/dhcp-1.Dockerfile +++ b/modules/network/dhcp-1/dhcp-1.Dockerfile @@ -1,37 +1,40 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Image name: test-run/dhcp-primary -FROM test-run/base:latest - -ARG MODULE_NAME=dhcp-1 -ARG MODULE_DIR=modules/network/$MODULE_NAME - -# Install all necessary packages -RUN apt-get install -y wget - -#Update the oui.txt file from ieee -RUN wget http://standards-oui.ieee.org/oui.txt -P /usr/local/etc/ - -# Install dhcp server -RUN apt-get install -y isc-dhcp-server radvd systemd - -# Copy over all configuration files -COPY $MODULE_DIR/conf /testrun/conf - -# Copy over all binary files -COPY $MODULE_DIR/bin /testrun/bin - -# Copy over all python files -COPY $MODULE_DIR/python /testrun/python \ No newline at end of file +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Image name: test-run/dhcp-primary +FROM test-run/base:latest + +ARG MODULE_NAME=dhcp-1 +ARG MODULE_DIR=modules/network/$MODULE_NAME + +#Update and get all additional requirements not contained in the base image +RUN apt-get update --fix-missing + +# Install all necessary packages +RUN apt-get install -y wget + +#Update the oui.txt file from ieee +RUN wget http://standards-oui.ieee.org/oui.txt -P /usr/local/etc/ + +# Install dhcp server +RUN apt-get install -y isc-dhcp-server radvd systemd + +# Copy over all configuration files +COPY $MODULE_DIR/conf /testrun/conf + +# Copy over all binary files +COPY $MODULE_DIR/bin /testrun/bin + +# Copy over all python files +COPY $MODULE_DIR/python /testrun/python diff --git a/modules/network/dhcp-2/dhcp-2.Dockerfile b/modules/network/dhcp-2/dhcp-2.Dockerfile index 153aa50e7..f2426c196 100644 --- a/modules/network/dhcp-2/dhcp-2.Dockerfile +++ b/modules/network/dhcp-2/dhcp-2.Dockerfile @@ -1,37 +1,40 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Image name: test-run/dhcp-primary -FROM test-run/base:latest - -ARG MODULE_NAME=dhcp-2 -ARG MODULE_DIR=modules/network/$MODULE_NAME - -# Install all necessary packages -RUN apt-get install -y wget - -#Update the oui.txt file from ieee -RUN wget http://standards-oui.ieee.org/oui.txt -P /usr/local/etc/ - -# Install dhcp server -RUN apt-get install -y isc-dhcp-server radvd systemd - -# Copy over all configuration files -COPY $MODULE_DIR/conf /testrun/conf - -# Copy over all binary files -COPY $MODULE_DIR/bin /testrun/bin - -# Copy over all python files -COPY $MODULE_DIR/python /testrun/python \ No newline at end of file +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Image name: test-run/dhcp-primary +FROM test-run/base:latest + +ARG MODULE_NAME=dhcp-2 +ARG MODULE_DIR=modules/network/$MODULE_NAME + +#Update and get all additional requirements not contained in the base image +RUN apt-get update --fix-missing + +# Install all necessary packages +RUN apt-get install -y wget + +#Update the oui.txt file from ieee +RUN wget http://standards-oui.ieee.org/oui.txt -P /usr/local/etc/ + +# Install dhcp server +RUN apt-get install -y isc-dhcp-server radvd systemd + +# Copy over all configuration files +COPY $MODULE_DIR/conf /testrun/conf + +# Copy over all binary files +COPY $MODULE_DIR/bin /testrun/bin + +# Copy over all python files +COPY $MODULE_DIR/python /testrun/python diff --git a/modules/network/ntp/bin/start_network_service b/modules/network/ntp/bin/start_network_service index 91129b18f..17a41309b 100644 --- a/modules/network/ntp/bin/start_network_service +++ b/modules/network/ntp/bin/start_network_service @@ -19,9 +19,15 @@ LOG_FILE="/runtime/network/ntp.log" echo Starting ntp +# Route internet traffic through gateway +ip route add default via 10.10.10.1 dev veth0 + #Create and set permissions on the log file touch $LOG_FILE chown $HOST_USER $LOG_FILE +# Move the config files to the correct location +cp /testrun/conf/chrony.conf /etc/chrony/ + #Start the NTP server python3 -u $PYTHON_SRC_DIR/ntp_server.py > $LOG_FILE diff --git a/modules/network/ntp/conf/chrony.conf b/modules/network/ntp/conf/chrony.conf new file mode 100644 index 000000000..c7fe108b5 --- /dev/null +++ b/modules/network/ntp/conf/chrony.conf @@ -0,0 +1,62 @@ +# Welcome to the chrony configuration file. See chrony.conf(5) for more +# information about usable directives. + +# Include configuration files found in /etc/chrony/conf.d. +confdir /etc/chrony/conf.d + +# This will use (up to): +# - 4 sources from ntp.ubuntu.com which some are ipv6 enabled +# - 2 sources from 2.ubuntu.pool.ntp.org which is ipv6 enabled as well +# - 1 source from [01].ubuntu.pool.ntp.org each (ipv4 only atm) +# This means by default, up to 6 dual-stack and up to 2 additional IPv4-only +# sources will be used. +# At the same time it retains some protection against one of the entries being +# down (compare to just using one of the lines). See (LP: #1754358) for the +# discussion. +# +# About using servers from the NTP Pool Project in general see (LP: #104525). +# Approved by Ubuntu Technical Board on 2011-02-08. +# See http://www.pool.ntp.org/join.html for more information. +pool time.google.com iburst maxsources 4 + +# Use time sources from DHCP. +sourcedir /run/chrony-dhcp + +# Use NTP sources found in /etc/chrony/sources.d. +sourcedir /etc/chrony/sources.d + +# This directive specify the location of the file containing ID/key pairs for +# NTP authentication. +keyfile /etc/chrony/chrony.keys + +# This directive0 specify the file into which chronyd will store the rate +# information. +driftfile /var/lib/chrony/chrony.drift + +# Save NTS keys and cookies. +ntsdumpdir /var/lib/chrony + +# Uncomment the following line to turn logging on. +#log tracking measurements statistics + +# Log files location. +logdir /var/log/chrony + +# Stop bad estimates upsetting machine clock. +maxupdateskew 100.0 + +# This directive enables kernel synchronisation (every 11 minutes) of the +# real-time clock. Note that it can’t be used along with the 'rtcfile' directive. +rtcsync + +# Step the system clock instead of slewing it if the adjustment is larger than +# one second, but only in the first three clock updates. +makestep 1 3 + +# Get TAI-UTC offset and leap seconds from the system tz database. +# This directive must be commented out when using time sources serving +# leap-smeared time. +leapsectz right/UTC + +# Enable NTP server and allow all traffic to this server +allow diff --git a/modules/network/ntp/ntp.Dockerfile b/modules/network/ntp/ntp.Dockerfile index cfd78c05e..aa6f63e3f 100644 --- a/modules/network/ntp/ntp.Dockerfile +++ b/modules/network/ntp/ntp.Dockerfile @@ -1,30 +1,36 @@ -# Copyright 2023 Google LLC -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -# Image name: test-run/ntp -FROM test-run/base:latest - -ARG MODULE_NAME=ntp -ARG MODULE_DIR=modules/network/$MODULE_NAME - -# Copy over all configuration files -COPY $MODULE_DIR/conf /testrun/conf - -# Copy over all binary files -COPY $MODULE_DIR/bin /testrun/bin - -# Copy over all python files -COPY $MODULE_DIR/python /testrun/python - -EXPOSE 123/udp +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Image name: test-run/ntp +FROM test-run/base:latest + +ARG MODULE_NAME=ntp +ARG MODULE_DIR=modules/network/$MODULE_NAME + +# Set DEBIAN_FRONTEND to noninteractive mode +ENV DEBIAN_FRONTEND=noninteractive + +# Install all necessary packages +RUN apt-get install -y chrony + +# Copy over all configuration files +COPY $MODULE_DIR/conf /testrun/conf + +# Copy over all binary files +COPY $MODULE_DIR/bin /testrun/bin + +# Copy over all python files +COPY $MODULE_DIR/python /testrun/python + +EXPOSE 123/udp diff --git a/modules/network/ntp/python/src/chronyd.py b/modules/network/ntp/python/src/chronyd.py new file mode 100644 index 000000000..b8ce7db56 --- /dev/null +++ b/modules/network/ntp/python/src/chronyd.py @@ -0,0 +1,49 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Contains all the necessary classes to maintain the +chronyd server booted from the chronyd.conf file""" +from common import logger +from common import util +import os + +LOG_NAME = 'chronyd' +LOGGER = None +PID_FILE='/run/chrony/chronyd.pid' + +class ChronydServer: + """Represents the chronyd server""" + + def __init__(self): + global LOGGER + LOGGER = logger.get_logger(LOG_NAME, 'ntp') + + def start(self): + LOGGER.info('Starting chronyd server') + response = util.run_command('chronyd', False) + LOGGER.info('chronyd server started: ' + str(response)) + return response + + def stop(self): + LOGGER.info('Stopping chronyd server') + with open(PID_FILE, 'r', encoding='UTF-8') as f: + pid = f.read() + response = util.run_command(f'kill {pid}', False) + LOGGER.info('chronyd server stopped: ' + str(response)) + return response + + def is_running(self): + LOGGER.info('Checking chronyd server') + running = os.path.exists(PID_FILE) + LOGGER.info('chronyd server status: ' + str(running)) + return running \ No newline at end of file diff --git a/modules/network/ntp/python/src/ntp_server.py b/modules/network/ntp/python/src/ntp_server.py index 4eda2b13e..14a3d9bac 100644 --- a/modules/network/ntp/python/src/ntp_server.py +++ b/modules/network/ntp/python/src/ntp_server.py @@ -11,328 +11,44 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - """NTP Server""" -import datetime -import socket -import struct -import time -import queue - -import threading -import select - -task_queue = queue.Queue() -stop_flag = False - - -def system_to_ntp_time(timestamp): - """Convert a system time to a NTP time. - - Parameters: - timestamp -- timestamp in system time - - Returns: - corresponding NTP time - """ - return timestamp + NTP.NTP_DELTA - - -def _to_int(timestamp): - """Return the integral part of a timestamp. - - Parameters: - timestamp -- NTP timestamp - - Retuns: - integral part - """ - return int(timestamp) - - -def _to_frac(timestamp, n=32): - """Return the fractional part of a timestamp. - - Parameters: - timestamp -- NTP timestamp - n -- number of bits of the fractional part - - Retuns: - fractional part - """ - return int(abs(timestamp - _to_int(timestamp)) * 2**n) - - -def _to_time(integ, frac, n=32): - """Return a timestamp from an integral and fractional part. - - Parameters: - integ -- integral part - frac -- fractional part - n -- number of bits of the fractional part - - Retuns: - timestamp - """ - return integ + float(frac) / 2**n - - -class NTPException(Exception): - """Exception raised by this module.""" - pass - - -class NTP: - """Helper class defining constants.""" - - _SYSTEM_EPOCH = datetime.date(*time.gmtime(0)[0:3]) - """system epoch""" - _NTP_EPOCH = datetime.date(1900, 1, 1) - """NTP epoch""" - NTP_DELTA = (_SYSTEM_EPOCH - _NTP_EPOCH).days * 24 * 3600 - """delta between system and NTP time""" - - REF_ID_TABLE = { - 'DNC': 'DNC routing protocol', - 'NIST': 'NIST public modem', - 'TSP': 'TSP time protocol', - 'DTS': 'Digital Time Service', - 'ATOM': 'Atomic clock (calibrated)', - 'VLF': 'VLF radio (OMEGA, etc)', - 'callsign': 'Generic radio', - 'LORC': 'LORAN-C radionavidation', - 'GOES': 'GOES UHF environment satellite', - 'GPS': 'GPS UHF satellite positioning', - } - """reference identifier table""" - - STRATUM_TABLE = { - 0: 'unspecified', - 1: 'primary reference', - } - """stratum table""" - - MODE_TABLE = { - 0: 'unspecified', - 1: 'symmetric active', - 2: 'symmetric passive', - 3: 'client', - 4: 'server', - 5: 'broadcast', - 6: 'reserved for NTP control messages', - 7: 'reserved for private use', - } - """mode table""" - LEAP_TABLE = { - 0: 'no warning', - 1: 'last minute has 61 seconds', - 2: 'last minute has 59 seconds', - 3: 'alarm condition (clock not synchronized)', - } - """leap indicator table""" - - -class NTPPacket: - """NTP packet class. - - This represents an NTP packet. - """ - - _PACKET_FORMAT = '!B B B b 11I' - """packet format to pack/unpack""" - - def __init__(self, version=4, mode=3, tx_timestamp=0): - """Constructor. - - Parameters: - version -- NTP version - mode -- packet mode (client, server) - tx_timestamp -- packet transmit timestamp - """ - self.leap = 0 - """leap second indicator""" - self.version = version - """version""" - self.mode = mode - """mode""" - self.stratum = 0 - """stratum""" - self.poll = 0 - """poll interval""" - self.precision = 0 - """precision""" - self.root_delay = 0 - """root delay""" - self.root_dispersion = 0 - """root dispersion""" - self.ref_id = 0 - """reference clock identifier""" - self.ref_timestamp = 0 - """reference timestamp""" - self.orig_timestamp = 0 - self.orig_timestamp_high = 0 - self.orig_timestamp_low = 0 - """originate timestamp""" - self.recv_timestamp = 0 - """receive timestamp""" - self.tx_timestamp = tx_timestamp - self.tx_timestamp_high = 0 - self.tx_timestamp_low = 0 - """tansmit timestamp""" - - def to_data(self): - """Convert this NTPPacket to a buffer that can be sent over a socket. - - Returns: - buffer representing this packet - - Raises: - NTPException -- in case of invalid field - """ - try: - packed = struct.pack( - NTPPacket._PACKET_FORMAT, - (self.leap << 6 | self.version << 3 | self.mode), - self.stratum, - self.poll, - self.precision, - _to_int(self.root_delay) << 16 | _to_frac(self.root_delay, 16), - _to_int(self.root_dispersion) << 16 - | _to_frac(self.root_dispersion, 16), - self.ref_id, - _to_int(self.ref_timestamp), - _to_frac(self.ref_timestamp), - #Change by lichen, avoid loss of precision - self.orig_timestamp_high, - self.orig_timestamp_low, - _to_int(self.recv_timestamp), - _to_frac(self.recv_timestamp), - _to_int(self.tx_timestamp), - _to_frac(self.tx_timestamp)) - except struct.error as exc: - raise NTPException('Invalid NTP packet fields.') from exc - return packed - - def from_data(self, data): - """Populate this instance from a NTP packet payload received from - the network. - - Parameters: - data -- buffer payload - - Raises: - NTPException -- in case of invalid packet format - """ - try: - unpacked = struct.unpack( - NTPPacket._PACKET_FORMAT, - data[0:struct.calcsize(NTPPacket._PACKET_FORMAT)]) - except struct.error as exc: - raise NTPException('Invalid NTP packet.') from exc - - self.leap = unpacked[0] >> 6 & 0x3 - self.version = unpacked[0] >> 3 & 0x7 - self.mode = unpacked[0] & 0x7 - self.stratum = unpacked[1] - self.poll = unpacked[2] - self.precision = unpacked[3] - self.root_delay = float(unpacked[4]) / 2**16 - self.root_dispersion = float(unpacked[5]) / 2**16 - self.ref_id = unpacked[6] - self.ref_timestamp = _to_time(unpacked[7], unpacked[8]) - self.orig_timestamp = _to_time(unpacked[9], unpacked[10]) - self.orig_timestamp_high = unpacked[9] - self.orig_timestamp_low = unpacked[10] - self.recv_timestamp = _to_time(unpacked[11], unpacked[12]) - self.tx_timestamp = _to_time(unpacked[13], unpacked[14]) - self.tx_timestamp_high = unpacked[13] - self.tx_timestamp_low = unpacked[14] - - def get_tx_timestamp(self): - return (self.tx_timestamp_high, self.tx_timestamp_low) - - def set_origin_timestamp(self, high, low): - self.orig_timestamp_high = high - self.orig_timestamp_low = low +from common import logger +from chronyd import ChronydServer +import time +LOG_NAME = 'ntp_server' -class RecvThread(threading.Thread): - """Thread class to recieve all requests""" +class NTPServer: + """Represents the NTP server""" def __init__(self): - threading.Thread.__init__(self) - #self.local_socket = local_socket - - def run(self): + global LOGGER + LOGGER = logger.get_logger(LOG_NAME, 'ntp') + self._chronyd = ChronydServer() + + def start(self): + return self._chronyd.start() + + def stop(self): + return self._chronyd.stop() + + def is_running(self): + return self._chronyd.is_running() + +if __name__ == '__main__': + ntp = NTPServer() + ntp.start() + # give some time for the server to start + running = False + for _ in range(10): + running = ntp.is_running() + if running: + break + else: + time.sleep(1) + # Enter loop if ntp server is running + if running: while True: - if stop_flag: - print('RecvThread Ended') - break - rlist, wlist, elist = select.select([local_socket], [], [], 1) # pylint: disable=unused-variable - if len(rlist) != 0: - print(f'Received {len(rlist)} packets') - for temp_socket in rlist: - try: - data, addr = temp_socket.recvfrom(1024) - recv_timestamp = system_to_ntp_time(time.time()) - task_queue.put((data, addr, recv_timestamp)) - except socket.error as msg: - print(msg) - - -class WorkThread(threading.Thread): - """Thread class to process all requests and respond""" - def __init__(self): - threading.Thread.__init__(self) - #self.local_socket = local_socket - - def run(self): - while True: - if stop_flag: - print('WorkThread Ended') - break - try: - data, addr, recv_timestamp = task_queue.get(timeout=1) - recv_packet = NTPPacket() - recv_packet.from_data(data) - timestamp_high, timestamp_low = recv_packet.get_tx_timestamp() - send_packet = NTPPacket(version=4, mode=4) - send_packet.stratum = 2 - send_packet.poll = 10 - - # send_packet.precision = 0xfa - # send_packet.root_delay = 0x0bfa - # send_packet.root_dispersion = 0x0aa7 - # send_packet.ref_id = 0x808a8c2c - - send_packet.ref_timestamp = recv_timestamp - 5 - send_packet.set_origin_timestamp(timestamp_high, timestamp_low) - send_packet.recv_timestamp = recv_timestamp - send_packet.tx_timestamp = system_to_ntp_time(time.time()) - local_socket.sendto(send_packet.to_data(), addr) - print(f'Sent to {addr[0]}:{addr[1]}') - except queue.Empty: - continue - - -listen_ip = '0.0.0.0' -listen_port = 123 -local_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) -local_socket.bind((listen_ip, listen_port)) -print('local socket: ', local_socket.getsockname()) -recvThread = RecvThread() -recvThread.start() -workThread = WorkThread() -workThread.start() - -while True: - try: - time.sleep(0.5) - except KeyboardInterrupt: - print('Exiting...') - stop_flag = True - recvThread.join() - workThread.join() - #local_socket.close() - print('Exited') - break + time.sleep(1) + else: + LOGGER.info('NTP server failed to start') diff --git a/modules/test/base/README.md b/modules/test/base/README.md new file mode 100644 index 000000000..e7f05d80e --- /dev/null +++ b/modules/test/base/README.md @@ -0,0 +1,19 @@ +# Base Test Module + +The base test module is a template image for other test modules. No actual tests are run by this module. + +Other test modules utilise this module as a base image to ensure consistency between the test modules and accuracy of the inputs and outputs. + +There is no requirement to re-use this module when creating your own test module, but it can speed up development. + +## What's inside? + +The ```bin``` folder contains multiple useful scripts that can be executed by test modules which use 'base' as a template. + +The ```config/module_config.json``` provides the name and description of the module, but prevents the image from being run as a container during the testing of the device. + +Within the ```python/src``` directory, basic logging and environment variables are provided to the test module. + +## Tests covered + +No tests are run by this module \ No newline at end of file diff --git a/modules/test/base/python/src/test_module.py b/modules/test/base/python/src/test_module.py index 519fb2433..47ec7b6c1 100644 --- a/modules/test/base/python/src/test_module.py +++ b/modules/test/base/python/src/test_module.py @@ -29,6 +29,7 @@ class TestModule: def __init__(self, module_name, log_name): self._module_name = module_name self._device_mac = os.environ['DEVICE_MAC'] + self._ipv4_addr = os.environ['IPV4_ADDR'] self._ipv4_subnet = os.environ['IPV4_SUBNET'] self._ipv6_subnet = os.environ['IPV6_SUBNET'] self._add_logger(log_name=log_name, module_name=module_name) @@ -74,16 +75,20 @@ def _get_device_test_module(self): return None def run_tests(self): + if self._config['config']['network']: self._device_ipv4_addr = self._get_device_ipv4() LOGGER.info('Device IP Resolved: ' + str(self._device_ipv4_addr)) + tests = self._get_tests() for test in tests: test_method_name = '_' + test['name'].replace('.', '_') result = None + test['start'] = datetime.now().isoformat() + if ('enabled' in test and test['enabled']) or 'enabled' not in test: - LOGGER.info('Attempting to run test: ' + test['name']) + LOGGER.debug('Attempting to run test: ' + test['name']) # Resolve the correct python method by test name and run test if hasattr(self, test_method_name): if 'config' in test: @@ -91,40 +96,30 @@ def run_tests(self): else: result = getattr(self, test_method_name)() else: - LOGGER.info(f'Test {test["name"]} not resolved. Skipping') + LOGGER.info(f'Test {test["name"]} not implemented. Skipping') result = None else: - LOGGER.info(f'Test {test["name"]} disabled. Skipping') + LOGGER.debug(f'Test {test["name"]} is disabled') + if result is not None: if isinstance(result, bool): - test['result'] = 'compliant' if result else 'non-compliant' + test['result'] = 'Compliant' if result else 'Non-Compliant' else: if result[0] is None: - test['result'] = 'skipped' - if len(result)>1: - test['result_details'] = result[1] + test['result'] = 'Skipped' + if len(result) > 1: + test['description'] = result[1] else: - test['result'] = 'compliant' if result[0] else 'non-compliant' - test['result_details'] = result[1] - else: - test['result'] = 'skipped' - - # Generate the short result description based on result value - if test['result'] == 'compliant': - test['result_description'] = test[ - 'short_description'] if 'short_description' in test else test[ - 'name'] + ' passed - see result details for more info' - elif test['result'] == 'non-compliant': - test['result_description'] = test[ - 'name'] + ' failed - see result details for more info' + test['result'] = 'Compliant' if result[0] else 'Non-Compliant' + test['description'] = result[1] else: - test['result_description'] = test[ - 'name'] + ' skipped - see result details for more info' + test['result'] = 'Skipped' test['end'] = datetime.now().isoformat() duration = datetime.fromisoformat(test['end']) - datetime.fromisoformat( test['start']) test['duration'] = str(duration) + json_results = json.dumps({'results': tests}, indent=2) self._write_results(json_results) diff --git a/modules/test/baseline/README.md b/modules/test/baseline/README.md new file mode 100644 index 000000000..c7c83f72e --- /dev/null +++ b/modules/test/baseline/README.md @@ -0,0 +1,21 @@ +# Baseline Test Module + +The baseline test module runs a test for each result status type. This is used for testing purposes - to ensure that the test framework is operational. + +This module is disabled by default when testing a physical device and there is no need for this to be enabled. + +## What's inside? + +The ```bin``` folder contains the startup script for the module. + +The ```config/module_config.json``` provides the name and description of the module, and specifies which tests will be caried out. + +Within the ```python/src``` directory, the below tests are executed. + +## Tests covered + +| ID | Description | Expected behavior | Required result +|---|---|---|---| +| baseline.compliant | Simulate a compliant test | A compliant test result is generated | Required | +| baseline.skipped | Simulate an skipped test | An skipped test result is generated | Skipped | +| baseline.non-compliant | Simulate a non-compliant test | A non-compliant test result is generated | Required | \ No newline at end of file diff --git a/modules/test/baseline/conf/module_config.json b/modules/test/baseline/conf/module_config.json index 83b920ea6..bdaff49c4 100644 --- a/modules/test/baseline/conf/module_config.json +++ b/modules/test/baseline/conf/module_config.json @@ -13,22 +13,22 @@ }, "tests":[ { - "name": "baseline.pass", - "description": "Simulate a compliant test", + "name": "baseline.compliant", + "test_description": "Simulate a compliant test", "expected_behavior": "A compliant test result is generated", "required_result": "Required" }, { - "name": "baseline.fail", - "description": "Simulate a non-compliant test", + "name": "baseline.non_compliant", + "test_description": "Simulate a non-compliant test", "expected_behavior": "A non-compliant test result is generated", "required_result": "Recommended" }, { - "name": "baseline.skip", - "description": "Simulate a skipped test", + "name": "baseline.skipped", + "test_description": "Simulate a skipped test", "expected_behavior": "A skipped test result is generated", - "required_result": "Roadmap" + "required_result": "Skipped" } ] } diff --git a/modules/test/baseline/python/src/baseline_module.py b/modules/test/baseline/python/src/baseline_module.py index 978f916fe..38da718de 100644 --- a/modules/test/baseline/python/src/baseline_module.py +++ b/modules/test/baseline/python/src/baseline_module.py @@ -27,17 +27,17 @@ def __init__(self, module): global LOGGER LOGGER = self._get_logger() - def _baseline_pass(self): + def _baseline_compliant(self): LOGGER.info('Running baseline pass test') LOGGER.info('Baseline pass test finished') return True, 'Baseline pass test ran successfully' - def _baseline_fail(self): - LOGGER.info('Running baseline fail test') - LOGGER.info('Baseline fail test finished') - return False, 'Baseline fail test ran successfully' + def _baseline_non_compliant(self): + LOGGER.info('Running baseline non-compliant test') + LOGGER.info('Baseline non-compliant test finished') + return False, 'Baseline non-compliant test ran successfully' - def _baseline_skip(self): - LOGGER.info('Running baseline skip test') - LOGGER.info('Baseline skip test finished') - return None, 'Baseline skip test ran successfully' + def _baseline_skipped(self): + LOGGER.info('Running baseline skipped test') + LOGGER.info('Baseline skipped test finished') + return None, 'Baseline skipped test ran successfully' diff --git a/modules/test/conn/README.md b/modules/test/conn/README.md new file mode 100644 index 000000000..48729f388 --- /dev/null +++ b/modules/test/conn/README.md @@ -0,0 +1,30 @@ +# Connection Test Module + +The connection test module runs a collection of tests around the IP and DHCP connectivity between the device and the provided network services. + +## What's inside? + +The ```bin``` folder contains the startup script for the module. + +The ```config/module_config.json``` file provides the name and description of the module, and specifies which tests will be caried out. + +Within the ```python/src``` directory, the below tests are executed. A few dhcp utility methods are included in ```python/src/dhcp_util.py```. + +## Tests covered + +| ID | Description | Expected Behavior | Required Result | +|------------------------------|----------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------| +| connection.dhcp.disconnect | The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request | The device is not set up with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds successfully to an ICMP echo (ping) request. | Required | +| connection.dhcp.disconnect_ip_change | Update device IP on the DHCP server and reconnect the device. Does the device receive the new IP address? | Device receives a new IP address within the range specified on the DHCP server. Device should respond to a ping on this new address. | Required | +| connection.dhcp_address | The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request | The device is not set up with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds successfully to an ICMP echo (ping) request. | Required | +| connection.mac_address | Check and note device physical address. | N/A | Required | +| connection.mac_oui | The device under test has a MAC address prefix that is registered against a known manufacturer. | The MAC address prefix is registered in the IEEE Organizationally Unique Identifier database. | Required | +| connection.private_address | The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets. | The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets: 10.0.0.0 - 10.255.255.255 (10/8 prefix), 172.16.0.0 - 172.31.255.255 (172.16/12 prefix), 192.168.0.0 - 192.168.255.255 (192.168/16 prefix). | Required | +| connection.shared_address | Ensure the device supports RFC 6598 IANA-Reserved IPv4 Prefix for Shared Address Space | The device under test accepts IP addresses within the range specified in RFC 6598 and communicates using these addresses. | Required | +| connection.private_address | The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets. | The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets: 10.0.0.0 - 10.255.255.255.255 (10/8 prefix), 172.16.0.0 - 172.31.255.255 (172.16/12 prefix), 192.168.0.0 - 192.168.255.255 (192.168/16 prefix). | Required | +| connection.single_ip | The network switch port connected to the device reports only one IP address for the device under test. | The device under test does not behave as a network switch and only requests one IP address. This test is to avoid that devices implement network switches that allow connecting strings of daisy-chained devices to one single network port, as this would not make 802.1x port-based authentication possible. | Required | +| connection.target_ping | The device under test responds to an ICMP echo (ping) request. | The device under test responds to an ICMP echo (ping) request. | Required | +| connection.ipaddr.ip_change | The device responds to a ping (ICMP echo request) to the new IP address it has received after the initial DHCP lease has expired. | If the lease expires before the client receives a DHCPACK, the client moves to the INIT state, MUST immediately stop any other network processing, and requires network initialization parameters as if the client were uninitialized. If the client then receives a DHCPACK allocating the client its previous network address, the client SHOULD continue network processing. If the client is given a new network address, it MUST NOT continue using the previous network address and SHOULD notify the local users of the problem. | Required | +| connection.ipaddr.dhcp_failover | The device has requested a DHCPREQUEST/REBIND to the DHCP failover server after the primary DHCP server has been brought down. | | Required | +| connection.ipv6_slaac | The device forms a valid IPv6 address as a combination of the IPv6 router prefix and the device interface identifier | The device under test complies with RFC4862 and forms a valid IPv6 SLAAC address. | Required | +| connection.ipv6_ping | The device responds to an IPv6 ping (ICMPv6 Echo) request to the SLAAC address | The device responds to the ping as per RFC4443 | Required | diff --git a/modules/test/conn/conf/module_config.json b/modules/test/conn/conf/module_config.json index c358ba1c2..5253c59f9 100644 --- a/modules/test/conn/conf/module_config.json +++ b/modules/test/conn/conf/module_config.json @@ -13,39 +13,27 @@ "timeout": 600 }, "tests": [ - { - "name": "connection.dhcp.disconnect", - "description": "The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request", - "expected_behavior": "The device is not setup with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds succesfully to an ICMP echo (ping) request.", - "required_result": "Required" - }, - { - "name": "connection.dhcp.disconnect_ip_change", - "description": "Update device IP on the DHCP server and reconnect the device. Does the device receive the new IP address?", - "expected_behavior": "Device recieves a new IP address within the range that is specified on the DHCP server. Device should respond to aping on this new address.", - "required_result": "Required" - }, { "name": "connection.dhcp_address", - "description": "The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request", + "test_description": "The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request", "expected_behavior": "The device is not setup with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds succesfully to an ICMP echo (ping) request.", "required_result": "Required" }, { "name": "connection.mac_address", - "description": "Check and note device physical address.", + "test_description": "Check and note device physical address.", "expected_behavior": "N/A", "required_result": "Required" }, { "name": "connection.mac_oui", - "description": "The device under test hs a MAC address prefix that is registered against a known manufacturer.", + "test_description": "The device under test hs a MAC address prefix that is registered against a known manufacturer.", "expected_behavior": "The MAC address prefix is registered in the IEEE Organizationally Unique Identifier database.", "required_result": "Required" }, { "name": "connection.private_address", - "description": "The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets.", + "test_description": "The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets.", "expected_behavior": "The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets. 10.0.0.0 - 10.255.255.255.255 (10/8 prefix). 172.16.0.0 - 172.31.255.255 (172.16/12 prefix). 192.168.0.0 - 192.168.255.255 (192.168/16 prefix)", "required_result": "Required", "config": { @@ -67,7 +55,7 @@ }, { "name": "connection.shared_address", - "description": "Ensure the device supports RFC 6598 IANA-Reserved IPv4 Prefix for Shared Address Space", + "test_description": "Ensure the device supports RFC 6598 IANA-Reserved IPv4 Prefix for Shared Address Space", "expected_behavior": "The device under test accepts IP addresses within the ranges specified in RFC 6598 and communicates using these addresses", "required_result": "Required", "config": { @@ -79,59 +67,39 @@ ] } }, - { - "name": "connection.private_address", - "description": "The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets.", - "expected_behavior": "The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets. 10.0.0.0 - 10.255.255.255.255 (10/8 prefix). 172.16.0.0 - 172.31.255.255 (172.16/12 prefix). 192.168.0.0 - 192.168.255.255 (192.168/16 prefix)", - "required_result": "Required", - "config": [ - { - "start": "10.0.0.100", - "end": "10.0.0.200" - }, - { - "start":"172.16.0.0", - "end":"172.16.255.255" - }, - { - "start":"192.168.0.0", - "end":"192.168.255.255" - } - ] - }, { "name": "connection.single_ip", - "description": "The network switch port connected to the device reports only one IP address for the device under test.", + "test_description": "The network switch port connected to the device reports only one IP address for the device under test.", "expected_behavior": "The device under test does not behave as a network switch and only requets one IP address. This test is to avoid that devices implement network switches that allow connecting strings of daisy chained devices to one single network port, as this would not make 802.1x port based authentication possible.", "required_result": "Required" }, { "name": "connection.target_ping", - "description": "The device under test responds to an ICMP echo (ping) request.", + "test_description": "The device under test responds to an ICMP echo (ping) request.", "expected_behavior": "The device under test responds to an ICMP echo (ping) request.", "required_result": "Required" }, { "name": "connection.ipaddr.ip_change", - "description": "The device responds to a ping (ICMP echo request) to the new IP address it has received after the initial DHCP lease has expired.", + "test_description": "The device responds to a ping (ICMP echo request) to the new IP address it has received after the initial DHCP lease has expired.", "expected_behavior": "If the lease expires before the client receiveds a DHCPACK, the client moves to INIT state, MUST immediately stop any other network processing and requires network initialization parameters as if the client were uninitialized. If the client then receives a DHCPACK allocating the client its previous network addres, the client SHOULD continue network processing. If the client is given a new network address, it MUST NOT continue using the previous network address and SHOULD notify the local users of the problem.", "required_result": "Required" }, { "name": "connection.ipaddr.dhcp_failover", - "description": "The device has requested a DHCPREQUEST/REBIND to the DHCP failover server after the primary DHCP server has been brought down.", + "test_description": "The device has requested a DHCPREQUEST/REBIND to the DHCP failover server after the primary DHCP server has been brought down.", "expected_behavior": "", "required_result": "Required" }, { "name": "connection.ipv6_slaac", - "description": "The device forms a valid IPv6 address as a combination of the IPv6 router prefix and the device interface identifier", + "test_description": "The device forms a valid IPv6 address as a combination of the IPv6 router prefix and the device interface identifier", "expected_behavior": "The device under test complies with RFC4862 and forms a valid IPv6 SLAAC address", "required_result": "Required" }, { "name": "connection.ipv6_ping", - "description": "The device responds to an IPv6 ping (ICMPv6 Echo) request to the SLAAC address", + "test_description": "The device responds to an IPv6 ping (ICMPv6 Echo) request to the SLAAC address", "expected_behavior": "The device responds to the ping as per RFC4443", "required_result": "Required" } diff --git a/modules/test/conn/python/src/connection_module.py b/modules/test/conn/python/src/connection_module.py index 248edc536..1ffb6ee4a 100644 --- a/modules/test/conn/python/src/connection_module.py +++ b/modules/test/conn/python/src/connection_module.py @@ -132,11 +132,14 @@ def _connection_single_ip(self): mac_addresses = set() LOGGER.info('Inspecting: ' + str(len(packets)) + ' packets') for packet in packets: - # Option[1] = message-type, option 3 = DHCPREQUEST - if DHCP in packet and packet[DHCP].options[0][1] == 3: - mac_address = packet[Ether].src - if not mac_address.startswith(TR_CONTAINER_MAC_PREFIX): - mac_addresses.add(mac_address.upper()) + if DHCP in packet: + for option in packet[DHCP].options: + # message-type, option 3 = DHCPREQUEST + if 'message-type' in option and option[1] == 3: + mac_address = packet[Ether].src + LOGGER.info('DHCPREQUEST detected MAC addres: ' + mac_address) + if not mac_address.startswith(TR_CONTAINER_MAC_PREFIX): + mac_addresses.add(mac_address.upper()) # Check if the device mac address is in the list of DHCPREQUESTs result = self._device_mac.upper() in mac_addresses @@ -144,9 +147,12 @@ def _connection_single_ip(self): # Check the unique MAC addresses to see if they match the device for mac_address in mac_addresses: - LOGGER.info('DHCPREQUEST from MAC address: ' + mac_address) result &= self._device_mac.upper() == mac_address - return result + + if result: + return result, 'Device is using a single IP address' + else: + return result, 'Device is using multiple IP addresses' def _connection_target_ping(self): LOGGER.info('Running connection.target_ping') @@ -157,9 +163,12 @@ def _connection_target_ping(self): if self._device_ipv4_addr is None: LOGGER.error('No device IP could be resolved') - sys.exit(1) + return False, 'Could not resolve device IP' else: - return self._ping(self._device_ipv4_addr) + if self._ping(self._device_ipv4_addr): + return True, 'Device responds to ping' + else: + return False, 'Device does not respond to ping' def _connection_ipaddr_ip_change(self): result = None @@ -167,7 +176,8 @@ def _connection_ipaddr_ip_change(self): if self._dhcp_util.setup_single_dhcp_server(): lease = self._dhcp_util.get_cur_lease(self._device_mac) if lease is not None: - LOGGER.info('Current device lease resolved: ' + str(lease)) + LOGGER.info('Current device lease resolved') + LOGGER.debug(str(lease)) # Figure out how to calculate a valid IP address ip_address = '10.10.10.30' if self._dhcp_util.add_reserved_lease(lease['hostname'], @@ -177,10 +187,10 @@ def _connection_ipaddr_ip_change(self): for _ in range(5): LOGGER.info('Pinging device at IP: ' + ip_address) if self._ping(ip_address): - LOGGER.info('Ping Success') + LOGGER.debug('Ping success') LOGGER.info('Reserved lease confirmed active in device') result = True, 'Device has accepted an IP address change' - LOGGER.info('Restoring DHCP failover configuration') + LOGGER.debug('Restoring DHCP failover configuration') break else: LOGGER.info('Device did not respond to ping') @@ -193,7 +203,7 @@ def _connection_ipaddr_ip_change(self): result = None, 'Device has no current DHCP lease' # Restore the network self._dhcp_util.restore_failover_dhcp_server() - LOGGER.info("Waiting 30 seconds for reserved lease to expire") + LOGGER.info('Waiting 30 seconds for reserved lease to expire') time.sleep(30) self._dhcp_util.get_new_lease(self._device_mac) else: @@ -210,7 +220,8 @@ def _connection_ipaddr_dhcp_failover(self): if primary_status and secondary_status: lease = self._dhcp_util.get_cur_lease(self._device_mac) if lease is not None: - LOGGER.info('Current device lease resolved: ' + str(lease)) + LOGGER.info('Current device lease resolved') + LOGGER.debug(str(lease)) if self._dhcp_util.is_lease_active(lease): # Shutdown the primary server if self._dhcp_util.stop_dhcp_server(dhcp_server_primary=True): @@ -279,10 +290,9 @@ def _connection_ipv6_slaac(self): def _connection_ipv6_ping(self): LOGGER.info('Running connection.ipv6_ping') result = None - if self._device_ipv6_addr is None: LOGGER.info('No IPv6 SLAAC address found. Cannot ping') - result = None, 'No IPv6 SLAAc address found. Cannot ping' + result = False, 'No IPv6 SLAAC address found. Cannot ping' else: if self._ping(self._device_ipv6_addr): LOGGER.info(f'Device responds to IPv6 ping on {self._device_ipv6_addr}') @@ -332,6 +342,8 @@ def setup_single_dhcp_server(self): else: return False, 'DHCP server stop command failed' + + # TODO: This code is unreachable. # Move primary DHCP server from failover into a single DHCP server config LOGGER.info('Configuring primary DHCP server') response = self.dhcp1_client.disable_failover() @@ -368,7 +380,7 @@ def _run_subnet_test(self, config): ranges = config['ranges'] else: LOGGER.error('No subnet ranges configured for test. Skipping') - return None, 'No subnet ranges configured for test. Skipping' + return None, 'No subnet ranges configured for test' response = self.dhcp1_client.get_dhcp_range() cur_range = {} @@ -404,7 +416,11 @@ def _run_subnet_test(self, config): final_result = result['result'] else: final_result &= result['result'] - final_result_details += result['details'] + '\n' + if result['result']: + final_result_details += result['details'] + '\n' + + if final_result: + final_result_details = 'All subnets are supported' try: # Restore failover configuration of DHCP servers @@ -419,11 +435,12 @@ def _run_subnet_test(self, config): LOGGER.info('Checking for new lease') lease = self._get_cur_lease() if lease is not None: - LOGGER.info('New Lease found: ' + str(lease)) + LOGGER.info('New lease found') + LOGGER.debug(str(lease)) LOGGER.info('Validating subnet for new lease...') in_range = self.is_ip_in_range(lease['ip'], cur_range['start'], cur_range['end']) - LOGGER.info('Lease within subnet: ' + str(in_range)) + LOGGER.debug('Lease within subnet: ' + str(in_range)) break else: LOGGER.info('New lease not found. Waiting to check again') @@ -438,7 +455,7 @@ def _test_subnet(self, subnet, lease): if self._change_subnet(subnet): expiration = datetime.strptime(lease['expires'], '%Y-%m-%d %H:%M:%S') time_to_expire = expiration - datetime.now() - LOGGER.info('Time until lease expiration: ' + str(time_to_expire)) + LOGGER.debug('Time until lease expiration: ' + str(time_to_expire)) LOGGER.info('Waiting for current lease to expire: ' + str(expiration)) if time_to_expire.total_seconds() > 0: time.sleep(time_to_expire.total_seconds() + @@ -448,7 +465,7 @@ def _test_subnet(self, subnet, lease): LOGGER.info('Checking for new lease') lease = self._get_cur_lease() if lease is not None: - LOGGER.info('New Lease found: ' + str(lease)) + LOGGER.info('New lease found: ' + str(lease)) LOGGER.info('Validating subnet for new lease...') in_range = self.is_ip_in_range(lease['ip'], subnet['start'], subnet['end']) @@ -457,6 +474,8 @@ def _test_subnet(self, subnet, lease): else: LOGGER.info('New lease not found. Waiting to check again') time.sleep(5) + else: + LOGGER.error('Failed to change subnet') def _wait_for_lease_expire(self, lease): expiration = datetime.strptime(lease['expires'], '%Y-%m-%d %H:%M:%S') @@ -472,15 +491,15 @@ def _change_subnet(self, subnet): LOGGER.info('Changing subnet to: ' + str(subnet)) response = self.dhcp1_client.set_dhcp_range(subnet['start'], subnet['end']) if response.code == 200: - LOGGER.info('Subnet change request accepted. Confirming change...') + LOGGER.debug('Subnet change request accepted. Confirming change...') response = self.dhcp1_client.get_dhcp_range() if response.code == 200: if response.start == subnet['start'] and response.end == subnet['end']: - LOGGER.info('Subnet change confirmed') + LOGGER.debug('Subnet change confirmed') return True - LOGGER.error('Failed to confirm subnet change') + LOGGER.debug('Failed to confirm subnet change') else: - LOGGER.error('Subnet change request failed.') + LOGGER.debug('Subnet change request failed.') return False def _get_cur_lease(self): diff --git a/modules/test/dns/conf/module_config.json b/modules/test/dns/conf/module_config.json index e00061047..a30e28077 100644 --- a/modules/test/dns/conf/module_config.json +++ b/modules/test/dns/conf/module_config.json @@ -14,19 +14,19 @@ "tests":[ { "name": "dns.network.hostname_resolution", - "description": "Verify the device sends DNS requests", + "test_description": "Verify the device sends DNS requests", "expected_behavior": "The device sends DNS requests.", "required_result": "Required" }, { "name": "dns.network.from_dhcp", - "description": "Verify the device allows for a DNS server to be entered automatically", + "test_description": "Verify the device allows for a DNS server to be entered automatically", "expected_behavior": "The device sends DNS requests to the DNS server provided by the DHCP server", "required_result": "Roadmap" }, { "name": "dns.mdns", - "description": "If the device has MDNS (or any kind of IP multicast), can it be disabled", + "test_description": "If the device has MDNS (or any kind of IP multicast), can it be disabled", "expected_behavior": "Device may send MDNS requests", "required_result": "Recommended" } diff --git a/modules/test/dns/python/src/dns_module.py b/modules/test/dns/python/src/dns_module.py index bc56c3718..e08d71f55 100644 --- a/modules/test/dns/python/src/dns_module.py +++ b/modules/test/dns/python/src/dns_module.py @@ -59,7 +59,7 @@ def _dns_network_from_dhcp(self): # Check if the device DNS traffic is to appropriate local # DHCP provided server tcpdump_filter = (f'dst port 53 and dst host {self._dns_server} ' + - 'and ether src {self._device_mac}') + f'and ether src {self._device_mac}') dns_packets_local = self._has_dns_traffic(tcpdump_filter=tcpdump_filter) # Check if the device sends any DNS traffic to non-DHCP provided server @@ -78,8 +78,8 @@ def _dns_network_from_dhcp(self): result = None, 'No DNS traffic detected from the device' return result - def _dns_network_from_device(self): - LOGGER.info('Running dns.network.from_device') + def _dns_network_hostname_resolution(self): + LOGGER.info('Running dns.network.hostname_resolution') result = None LOGGER.info('Checking DNS traffic from device: ' + self._device_mac) @@ -95,6 +95,7 @@ def _dns_network_from_device(self): result = False, 'No DNS traffic detected from the device' return result + ## TODO: This test should always return 'Informational' result def _dns_mdns(self): LOGGER.info('Running dns.mdns') result = None @@ -107,7 +108,7 @@ def _dns_mdns(self): result = True, 'MDNS traffic detected from device' else: LOGGER.info('No MDNS traffic detected from the device') - result = None, 'No MDNS traffic detected from the device' + result = True, 'No MDNS traffic detected from the device' return result def _exec_tcpdump(self, tcpdump_filter, capture_file): diff --git a/modules/test/nmap/conf/module_config.json b/modules/test/nmap/conf/module_config.json index 8a90febc1..1f7ae74a9 100644 --- a/modules/test/nmap/conf/module_config.json +++ b/modules/test/nmap/conf/module_config.json @@ -13,166 +13,333 @@ }, "tests": [ { - "name": "security.nmap.ports", - "description": "Run an nmap scan of open ports", - "expected_behavior": "Report all open ports", + "name": "security.services.ftp", + "test_description": "Check FTP port 20/21 is disabled and FTP is not running on any port", + "expected_behavior": "There is no FTP service running on any port", + "required_result": "Required", "config": { - "security.services.ftp": { - "tcp_ports": { - "20": { - "allowed": false, - "description": "File Transfer Protocol (FTP) Server Data Transfer" - }, - "21": { - "allowed": false, - "description": "File Transfer Protocol (FTP) Server Data Transfer" - } - }, - "description": "Check FTP port 20/21 is disabled and FTP is not running on any port", - "expected_behavior": "There is no FTP service running on any port", - "required_result": "Required" - }, - "security.ssh.version": { - "tcp_ports": { - "22": { - "allowed": true, - "description": "Secure Shell (SSH) server", - "version": "2.0" - } - }, - "description": "If the device is running a SSH server ensure it is SSHv2", - "expected_behavior": "SSH server is not running or server is SSHv2", - "required_result": "Required" - }, - "security.services.telnet": { - "tcp_ports": { - "23": { - "allowed": false, - "description": "Telnet Server" - } - }, - "description": "Check TELNET port 23 is disabled and TELNET is not running on any port", - "expected_behavior": "There is no FTP service running on any port", - "required_result": "Required" - }, - "security.services.smtp": { - "tcp_ports": { - "25": { - "allowed": false, - "description": "Simple Mail Transfer Protocol (SMTP) Server" - }, - "465": { - "allowed": false, - "description": "Simple Mail Transfer Protocol over SSL (SMTPS) Server" - }, - "587": { - "allowed": false, - "description": "Simple Mail Transfer Protocol via TLS (SMTPS) Server" - } - }, - "description": "Check SMTP ports 25, 465 and 587 are not enabled and SMTP is not running on any port.", - "expected_behavior": "There is no smtp service running on any port", - "required_result": "Required" - }, - "security.services.http": { - "tcp_ports": { - "80": { - "service_scan": { - "script": "http-methods" - }, - "allowed": false, - "description": "Administrative Insecure Web-Server" - } - }, - "description": "Check that there is no HTTP server running on any port", - "expected_behavior": "Device is unreachable on port 80 (or any other port) and only responds to HTTPS requests on port 443 (or any other port if HTTP is used at all)", - "required_result": "Required" - }, - "security.services.pop": { - "tcp_ports": { - "110": { - "allowed": false, - "description": "Post Office Protocol v3 (POP3) Server" - } - }, - "description": "Check POP port 110 is disalbed and POP is not running on any port", - "expected_behavior": "There is no pop service running on any port", - "required_result": "Required" - }, - "security.services.imap": { - "tcp_ports": { - "143": { - "allowed": false, - "description": "Internet Message Access Protocol (IMAP) Server" - } - }, - "description": "Check IMAP port 143 is disabled and IMAP is not running on any port", - "expected_behavior": "There is no imap service running on any port", - "required_result": "Required" - }, - "security.services.snmpv3": { - "tcp_ports": { - "161": { - "allowed": false, - "description": "Simple Network Management Protocol (SNMP)" - }, - "162": { - "allowed": false, - "description": "Simple Network Management Protocol (SNMP) Trap" - } - }, - "udp_ports": { - "161": { - "allowed": false, - "description": "Simple Network Management Protocol (SNMP)" - }, - "162": { - "allowed": false, - "description": "Simple Network Management Protocol (SNMP) Trap" - } - }, - "description": "Check SNMP port 161/162 is disabled. If SNMP is an essential service, check it supports version 3", - "expected_behavior": "Device is unreachable on port 161 (or any other port) and device is unreachable on port 162 (or any other port) unless SNMP is essential in which case it is SNMPv3 is used.", - "required_result": "Required" - }, - "security.services.vnc": { - "tcp_ports": { - "5800": { - "allowed": false, - "description": "Virtual Network Computing (VNC) Remote Frame Buffer Protocol Over HTTP" - }, - "5500": { - "allowed": false, - "description": "Virtual Network Computing (VNC) Remote Frame Buffer Protocol" - } - }, - "description": "Check VNC is disabled on any port", - "expected_behavior": "Device cannot be accessed /connected to via VNC on any port", - "required_result": "Required" - }, - "security.services.tftp": { - "udp_ports": { - "69": { - "allowed": false, - "description": "Trivial File Transfer Protocol (TFTP) Server" - } - }, - "description": "Check TFTP port 69 is disabled (UDP)", - "expected_behavior": "There is no tftp service running on any port", - "required_result": "Required" - }, - "ntp.network.ntp_server": { - "udp_ports": { - "123": { - "allowed": false, - "description": "Network Time Protocol (NTP) Server" - } - }, - "description": "Check NTP port 123 is disabled and the device is not operating as an NTP server", - "expected_behavior": "The device dos not respond to NTP requests when it's IP is set as the NTP server on another device" - } - }, - "required_result": "Required" + "services": [ + "ftp", + "ftp-data" + ], + "ports": [ + { + "number": 20, + "type": "tcp" + }, + { + "number": 20, + "type": "udp" + }, + { + "number": 21, + "type": "tcp" + }, + { + "number": 21, + "type": "udp" + } + ] + } + }, + { + "name": "security.ssh.version", + "test_description": "If the device is running a SSH server ensure it is SSHv2", + "expected_behavior": "SSH server is not running or server is SSHv2", + "required_result": "Required", + "config": { + "services": ["ssh"], + "ports": [ + { + "number": 22, + "type": "tcp" + } + ], + "version": "protocol 2.0" + } + }, + { + "name": "security.services.telnet", + "test_description": "Check TELNET port 23 is disabled and TELNET is not running on any port", + "expected_behavior": "There is no FTP service running on any port", + "required_result": "Required", + "config": { + "services": [ + "telnet" + ], + "ports": [ + { + "number": 23, + "type": "tcp" + }, + { + "number": 23, + "type": "udp" + } + ] + } + }, + { + "name": "security.services.smtp", + "test_description": "Check SMTP ports 25, 465 and 587 are not enabled and SMTP is not running on any port.", + "expected_behavior": "There is no smtp service running on any port", + "required_result": "Required", + "config": { + "services": [ + "smtp" + ], + "ports": [ + { + "number": 25, + "type": "tcp" + }, + { + "number": 465, + "type": "tcp" + }, + { + "number": 587, + "type": "tcp" + } + ] + } + }, + { + "name": "security.services.http", + "test_description": "Check that there is no HTTP server running on any port", + "expected_behavior": "Device is unreachable on port 80 (or any other port) and only responds to HTTPS requests on port 443 (or any other port if HTTP is used at all)", + "required_result": "Required", + "config": { + "services": [ + "http" + ], + "ports": [ + { + "number": 80, + "type": "tcp" + }, + { + "number": 80, + "type": "udp" + }, + { + "number": 443, + "type": "tcp", + "allowed": true + }, + { + "number": 443, + "type": "udp", + "allowed": true + } + ] + } + }, + { + "name": "security.services.pop", + "test_description": "Check POP ports 109 and 110 are disabled and POP is not running on any port", + "expected_behavior": "There is no pop service running on any port", + "required_result": "Required", + "config": { + "services": [ + "pop2", + "pop3", + "pop3s" + ], + "ports": [ + { + "number": 109, + "type": "tcp" + }, + { + "number": 109, + "type": "udp" + }, + { + "number": 110, + "type": "tcp" + }, + { + "number": 110, + "type": "udp" + }, + { + "number": 995, + "type": "tcp" + }, + { + "number": 995, + "type": "udp" + } + ] + } + }, + { + "name": "security.services.imap", + "test_description": "Check IMAP port 143 is disabled and IMAP is not running on any port", + "expected_behavior": "There is no imap service running on any port", + "required_result": "Required", + "config": { + "services": [ + "imap", + "imap3", + "imap4-ssl" + ], + "ports": [ + { + "number": 143, + "type": "tcp" + }, + { + "number": 143, + "type": "udp" + }, + { + "number": 220, + "type": "tcp" + }, + { + "number": 220, + "type": "udp" + }, + { + "number": 585, + "type": "tcp" + }, + { + "number": 585, + "type": "udp" + }, + { + "number": 993, + "type": "tcp" + }, + { + "number": 993, + "type": "udp" + } + ] + } + }, + { + "name": "security.services.snmpv3", + "test_description": "Check SNMP port 161/162 is disabled. If SNMP is an essential service, check it supports version 3", + "expected_behavior": "Device is unreachable on port 161 (or any other port) and device is unreachable on port 162 (or any other port) unless SNMP is essential in which case it is SNMPv3 is used.", + "required_result": "Required", + "config": { + "services": [ + "snmp" + ], + "ports": [ + { + "number": 161, + "type": "tcp" + }, + { + "number": 161, + "type": "udp" + } + ] + } + }, + { + "name": "security.services.vnc", + "test_description": "Check VNC is disabled on any port", + "expected_behavior": "Device cannot be accessed /connected to via VNC on any port", + "required_result": "Required", + "config": { + "services": [ + "vnc", + "vnc-1", + "vnc-2", + "vnc-3", + "vnc-http", + "vnc-http-1", + "vnc-http-2", + "vnc-http-3" + ], + "ports": [ + { + "number": 5800, + "type": "tcp" + }, + { + "number": 5801, + "type": "tcp" + }, + { + "number": 5802, + "type": "tcp" + }, + { + "number": 5803, + "type": "tcp" + }, + { + "number": 5900, + "type": "tcp" + }, + { + "number": 5901, + "type": "tcp" + }, + { + "number": 5902, + "type": "tcp" + }, + { + "number": 5903, + "type": "tcp" + } + ] + } + }, + { + "name": "security.services.tftp", + "test_description": "Check TFTP port 69 is disabled (UDP)", + "expected_behavior": "There is no tftp service running on any port", + "required_result": "Required", + "config": { + "services": [ + "tftp", + "tftps" + ], + "ports": [ + { + "number": 69, + "type": "tcp" + }, + { + "number": 69, + "type": "udp" + }, + { + "number": 3713, + "type": "tcp" + }, + { + "number": 3713, + "type": "udp" + } + ] + } + }, + { + "name": "ntp.network.ntp_server", + "test_description": "Check NTP port 123 is disabled and the device is not operating as an NTP server", + "expected_behavior": "The device dos not respond to NTP requests when it's IP is set as the NTP server on another device", + "required_result": "Required", + "config": { + "services": [ + "ntp" + ], + "ports": [ + { + "number": 123, + "type": "udp" + } + ] + } } ] } diff --git a/modules/test/nmap/python/src/nmap_module.py b/modules/test/nmap/python/src/nmap_module.py index 6bcbd141a..517dc94f9 100644 --- a/modules/test/nmap/python/src/nmap_module.py +++ b/modules/test/nmap/python/src/nmap_module.py @@ -18,7 +18,6 @@ import json import threading import xmltodict -import re from test_module import TestModule LOG_NAME = "test_nmap" @@ -30,361 +29,256 @@ class NmapModule(TestModule): def __init__(self, module): super().__init__(module_name=module, log_name=LOG_NAME) - self._unallowed_ports = [] self._scan_tcp_results = None self._udp_tcp_results = None - self._script_scan_results = None + self._scan_results = {} + global LOGGER LOGGER = self._get_logger() + self._run_nmap() - def _security_nmap_ports(self, config): - LOGGER.info("Running security.nmap.ports test") - result = None - - # Delete the enabled key from the config if it exists - # to prevent it being treated as a test key - if "enabled" in config: - del config["enabled"] - - if self._device_ipv4_addr is not None: - # Run the monitor method asynchronously to keep this method non-blocking - self._tcp_scan_thread = threading.Thread(target=self._scan_tcp_ports, - args=(config, )) - self._udp_scan_thread = threading.Thread(target=self._scan_udp_ports, - args=(config, )) - self._script_scan_thread = threading.Thread(target=self._scan_scripts, - args=(config, )) - - self._tcp_scan_thread.daemon = True - self._udp_scan_thread.daemon = True - self._script_scan_thread.daemon = True - - self._tcp_scan_thread.start() - self._udp_scan_thread.start() - self._script_scan_thread.start() - - while self._tcp_scan_thread.is_alive() or self._udp_scan_thread.is_alive( - ) or self._script_scan_thread.is_alive(): - time.sleep(1) - - LOGGER.debug("TCP scan results: " + str(self._scan_tcp_results)) - LOGGER.debug("UDP scan results: " + str(self._scan_udp_results)) - LOGGER.debug("Service scan results: " + str(self._script_scan_results)) - self._process_port_results(tests=config) - LOGGER.info("Unallowed Ports Detected: " + str(self._unallowed_ports)) - self._check_unallowed_port(self._unallowed_ports,config) - LOGGER.info("Unallowed Ports: " + str(self._unallowed_ports)) - if len(self._unallowed_ports) > 0: - result = False, 'Some allowed ports detected: ' + str(self._unallowed_ports) - else: - result = True, 'No unallowed ports detected' - else: - LOGGER.info("Device ip address not resolved, skipping") - result = None, "Device ip address not resolved" - return result + def _run_nmap(self): + LOGGER.info("Running nmap module") + + # Run the monitor method asynchronously to keep this method non-blocking + self._tcp_scan_thread = threading.Thread(target=self._scan_tcp_ports) + self._udp_scan_thread = threading.Thread(target=self._scan_udp_ports) + + self._tcp_scan_thread.daemon = True + self._udp_scan_thread.daemon = True + + self._tcp_scan_thread.start() + self._udp_scan_thread.start() + + while (self._tcp_scan_thread.is_alive() or + self._udp_scan_thread.is_alive()): + time.sleep(1) + + LOGGER.debug("TCP scan results: " + str(self._scan_tcp_results)) + LOGGER.debug("UDP scan results: " + str(self._scan_udp_results)) + + self._process_port_results() + + def _process_port_results(self): - def _process_port_results(self, tests): - scan_results = {} if self._scan_tcp_results is not None: - scan_results.update(self._scan_tcp_results) + self._scan_results.update(self._scan_tcp_results) if self._scan_udp_results is not None: - scan_results.update(self._scan_udp_results) - if self._script_scan_results is not None: - scan_results.update(self._script_scan_results) - - self._check_unknown_ports(tests=tests,scan_results=scan_results) - - for test in tests: - LOGGER.info("Checking scan results for test: " + str(test)) - self._check_scan_results(test_config=tests[test],scan_results=scan_results) - - def _check_unknown_ports(self,tests,scan_results): - """ Check if any of the open ports detected are not defined - in the test configurations. If an open port is detected - without a configuration associated with it, the default behavior - is to mark it as an unallowed port. - """ - known_ports = [] - for test in tests: - if "tcp_ports" in tests[test]: - for port in tests[test]['tcp_ports']: - known_ports.append(port) - if "udp_ports" in tests[test]: - for port in tests[test]['udp_ports']: - known_ports.append(port) - - for port_result in scan_results: - if not port_result in known_ports: - LOGGER.info("Unknown port detected: " + port_result) - unallowed_port = {'port':port_result, - 'service':scan_results[port_result]['service'], - 'tcp_udp':scan_results[port_result]['tcp_udp']} - #self._unallowed_ports.append(unallowed_port) - self._add_unknown_ports(tests,unallowed_port) - - def _add_unknown_ports(self,tests,unallowed_port): - known_service = False - result = {'description':"Undefined port",'allowed':False} - if unallowed_port['tcp_udp'] == 'tcp': - port_style = 'tcp_ports' - elif unallowed_port['tcp_udp'] == 'udp': - port_style = 'udp_ports' - - LOGGER.info("Unknown Port Service: " + unallowed_port['service']) - for test in tests: - LOGGER.debug("Checking for known service: " + test) - # Create a regular expression pattern to match the variable at the - # end of the string - port_service = r"\b" + re.escape(unallowed_port['service']) + r"\b$" - service_match = re.search(port_service, test) - if service_match: - LOGGER.info("Service Matched: " + test) - known_service=True - for test_port in tests[test][port_style]: - if "version" in tests[test][port_style][test_port]: - result['version'] = tests[test][port_style][test_port]['version'] - if "description" in tests[test][port_style][test_port]: - result['description'] = tests[test][port_style][test_port]['description'] - result['inherited_from'] = test_port - if tests[test][port_style][test_port]['allowed']: - result['allowed'] = True - break - tests[test][port_style][unallowed_port['port']]=result - break - - if not known_service: - service_name = "security.services.unknown." + str(unallowed_port['port']) - unknown_service = {port_style:{unallowed_port['port']:result}} - tests[service_name]=unknown_service - - - def _check_scan_results(self,test_config,scan_results): - if "tcp_ports" in test_config: - port_config = test_config["tcp_ports"] - self._check_scan_result(port_config=port_config,scan_results=scan_results) - if "udp_ports" in test_config: - port_config = test_config["udp_ports"] - self._check_scan_result(port_config=port_config,scan_results=scan_results) - - - def _check_scan_result(self,port_config,scan_results): - if port_config is not None: - for port, config in port_config.items(): - result = None - LOGGER.info("Checking port: " + str(port)) - LOGGER.debug("Port config: " + str(config)) - if port in scan_results: - if scan_results[port]["state"] == "open": - if not config["allowed"]: - LOGGER.info("Unallowed port open") - self._unallowed_ports.append( - {"port":str(port), - "service":str(scan_results[port]["service"]), - 'tcp_udp':scan_results[port]['tcp_udp']} - ) - result = False - else: - LOGGER.info("Allowed port open") - if "version" in config and "version" in scan_results[port]: - version_check = self._check_version(scan_results[port]["service"], - scan_results[port]["version"],config["version"]) - if version_check is not None: - result = version_check - else: - result = True - else: - result = True - else: - LOGGER.info("Port is closed") - result = True - else: - LOGGER.info("Port not detected, closed") - result = True - - if result is not None: - config["result"] = "compliant" if result else "non-compliant" - else: - config["result"] = "skipped" - - def _check_unallowed_port(self,unallowed_ports,tests): - service_allowed=False - allowed = False - version = None - service = None - for port in unallowed_ports: - LOGGER.info('Checking unallowed port: ' + port['port']) - LOGGER.info('Looking for service: ' + port['service']) - LOGGER.debug('Unallowed Port Config: ' + str(port)) - if port['tcp_udp'] == 'tcp': - port_style = 'tcp_ports' - elif port['tcp_udp'] == 'udp': - port_style = 'udp_ports' - for test in tests: - LOGGER.debug('Checking test: ' + str(test)) - # Create a regular expression pattern to match the variable at the - # end of the string - port_service = r"\b" + re.escape(port['service']) + r"\b$" - service_match = re.search(port_service, test) - if service_match: - LOGGER.info("Service Matched: " + test) - service_config = tests[test] - service = port['service'] - for service_port in service_config[port_style]: - port_config = service_config[port_style][service_port] - service_allowed |= port_config['allowed'] - version = port_config['version'] if 'version' in port_config else None - if service_allowed: - LOGGER.info("Unallowed port detected for allowed service: " + service) - if version is not None: - allowed = self._check_version(service=service, - version_detected=self._scan_tcp_results[port['port']]['version'], - version_expected=version) - else: - allowed = True - if allowed: - LOGGER.info("Unallowed port exception for approved service: " + port['port']) - for u_port in self._unallowed_ports: - if port['port'] in u_port['port']: - self._unallowed_ports.remove(u_port) - break - break - - def _check_version(self,service,version_detected,version_expected): - """Check if the version specified for the service matches what was - detected by nmap. Since there is no consistency in how nmap service - results are returned, each service that needs a checked must be - implemented individually. If a service version is requested - that is not implemented, this test will provide a skip (None) - result. - """ - LOGGER.info("Checking version for service: " + service) - LOGGER.info("NMAP Version Detected: " + version_detected) - LOGGER.info("Version Expected: " + version_expected) - version_check = None - match service: - case "ssh": - version_check = f"protocol {version_expected}" in version_detected - case _: - LOGGER.info("No version check implemented for service: " + service + ". Skipping") - LOGGER.info("Version check result: " + str(version_check)) - return version_check - - def _scan_scripts(self, tests): - scan_results = {} - LOGGER.info("Checking for scan scripts") - for test in tests: - test_config = tests[test] - if "tcp_ports" in test_config: - for port in test_config["tcp_ports"]: - port_config = test_config["tcp_ports"][port] - if "service_scan" in port_config: - LOGGER.info("Service Scan Detected for: " + str(port)) - svc = port_config["service_scan"] - result = self._scan_tcp_with_script(svc["script"]) - scan_results.update(result) - if "udp_ports" in test_config: - for port in test_config["udp_ports"]: - if "service_scan" in port: - LOGGER.info("Service Scan Detected for: " + str(port)) - svc = port["service_scan"] - result = self._scan_udp_with_script(svc["script"], port) - scan_results.update(result) - self._script_scan_results = scan_results - - def _scan_tcp_with_script(self, script_name, ports=None): - LOGGER.info("Running TCP nmap scan with script " + script_name) - scan_options = " -v -n T3 --host-timeout=6m -A --script " + script_name - port_options = " --open " - if ports is None: - port_options += " -p- " - else: - port_options += " -p" + ports + " " - results_file = f"/runtime/output/{self._module_name}-script_name.log" - nmap_options = scan_options + port_options + " " + results_file + " -oX -" - nmap_results = util.run_command("nmap " + nmap_options + " " + - self._device_ipv4_addr)[0] - LOGGER.info("Nmap TCP script scan complete") - nmap_results_json = self._nmap_results_to_json(nmap_results) - return self._process_nmap_json_results(nmap_results_json=nmap_results_json) - - def _scan_udp_with_script(self, script_name, ports=None): - LOGGER.info("Running UDP nmap scan with script " + script_name) - scan_options = " --sU -Pn -n --script " + script_name - port_options = " --open " - if ports is None: - port_options += " -p- " - else: - port_options += " -p" + ports + " " - nmap_options = scan_options + port_options + " -oX - " - nmap_results = util.run_command("nmap " + nmap_options + - self._device_ipv4_addr)[0] - LOGGER.info("Nmap UDP script scan complete") - nmap_results_json = self._nmap_results_to_json(nmap_results) - return self._process_nmap_json_results(nmap_results_json=nmap_results_json) + self._scan_results.update(self._scan_udp_results) - def _scan_tcp_ports(self, tests): - max_port = 65535 + def _scan_tcp_ports(self): + max_port = 1000 LOGGER.info("Running nmap TCP port scan") nmap_results = util.run_command( f"""nmap --open -sT -sV -Pn -v -p 1-{max_port} - --version-intensity 7 -T4 -oX - {self._device_ipv4_addr}""")[0] + --version-intensity 7 -T4 -oX - {self._ipv4_addr}""")[0] LOGGER.info("TCP port scan complete") nmap_results_json = self._nmap_results_to_json(nmap_results) self._scan_tcp_results = self._process_nmap_json_results( nmap_results_json=nmap_results_json) - def _scan_udp_ports(self, tests): + def _scan_udp_ports(self): + ports = [] - for test in tests: - test_config = tests[test] - if "udp_ports" in test_config: - for port in test_config["udp_ports"]: - ports.append(port) + + for test in self._get_tests(): + if "config" not in test: + continue + test_config = test["config"] + if "ports" not in test_config: + continue + + for port in test_config["ports"]: + if port["type"] == "udp": + ports.append(str(port["number"])) + if len(ports) > 0: port_list = ",".join(ports) LOGGER.info("Running nmap UDP port scan") - LOGGER.info("UDP ports: " + str(port_list)) + LOGGER.debug("UDP ports: " + str(port_list)) nmap_results = util.run_command( - f"nmap -sU -sV -p {port_list} -oX - {self._device_ipv4_addr}")[0] + f"nmap -sU -sV -p {port_list} -oX - {self._ipv4_addr}")[0] LOGGER.info("UDP port scan complete") nmap_results_json = self._nmap_results_to_json(nmap_results) self._scan_udp_results = self._process_nmap_json_results( nmap_results_json=nmap_results_json) - def _nmap_results_to_json(self,nmap_results): + def _nmap_results_to_json(self, nmap_results): try: - xml_data = xmltodict.parse(nmap_results) - json_data = json.dumps(xml_data, indent=4) - return json.loads(json_data) + xml_data = xmltodict.parse(nmap_results) + json_data = json.dumps(xml_data, indent=4) + return json.loads(json_data) except Exception as e: - LOGGER.error(f"Error parsing Nmap output: {e}") + LOGGER.error(f"Error parsing Nmap output: {e}") - def _process_nmap_json_results(self,nmap_results_json): - LOGGER.debug("nmap results\n" + json.dumps(nmap_results_json,indent=2)) + def _process_nmap_json_results(self, nmap_results_json): results = {} + if "host" not in nmap_results_json["nmaprun"]: + return results if "ports" in nmap_results_json["nmaprun"]["host"]: - ports = nmap_results_json["nmaprun"]["host"]["ports"] + ports = nmap_results_json["nmaprun"]["host"]["ports"] # Checking if an object is a JSON object if isinstance(ports["port"], dict): - results.update(self._json_port_to_dict(ports["port"])) + results.update(self._json_port_to_dict(ports["port"])) elif isinstance(ports["port"], list): - for port in ports["port"]: - results.update(self._json_port_to_dict(port)) + for port in ports["port"]: + results.update(self._json_port_to_dict(port)) + print(str(results)) return results - def _json_port_to_dict(self,port_json): + def _json_port_to_dict(self, port_json): port_result = {} port = {} + port["number"] = port_json["@portid"] port["tcp_udp"] = port_json["@protocol"] port["state"] = port_json["state"]["@state"] port["service"] = port_json["service"]["@name"] port["version"] = "" if "@version" in port_json["service"]: port["version"] += port_json["service"]["@version"] - if "@extrainfo" in port_json["service"]: - port["version"] += " " + port_json["service"]["@extrainfo"] - port_result = {port_json["@portid"]:port} - return port_result \ No newline at end of file + if "@extrainfo" in port_json["service"]: + port["version"] += " " + port_json["service"]["@extrainfo"] + port_result = {port_json["@portid"] + port["tcp_udp"]:port} + return port_result + + def _check_results(self, ports, services): + + LOGGER.info("Checking results") + + match_ports = [] + + for open_port, open_port_info in self._scan_results.items(): + + for port in ports: + allowed = True if 'allowed' in port and port['allowed'] else False + if (int(open_port_info["number"]) == int(port["number"]) and + open_port_info["tcp_udp"] == port["type"] and + open_port_info["state"] == "open"): + LOGGER.debug("Found open port: " + str(port["number"]) + + "/" + open_port_info["tcp_udp"] + + " = " + open_port_info["state"]) + if not allowed: + match_ports.append(open_port_info["number"] + "/" + + open_port_info["tcp_udp"]) + + if (open_port_info["service"] in services and + (open_port + "/" + open_port_info["tcp_udp"]) not in match_ports and + open_port_info["state"] == "open"): + LOGGER.debug("Found service " + open_port_info["service"] + + " on port " + str(open_port) + "/" + + open_port_info["tcp_udp"]) + + if not allowed: + match_ports.append(open_port_info["number"] + "/" + + open_port_info["tcp_udp"]) + + return match_ports + + def _security_services_ftp(self, config): + LOGGER.info("Running security.services.ftp") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No FTP server found" + else: + return False, f"Found FTP server running on port {', '.join(open_ports)}" + + def _security_services_telnet(self, config): + LOGGER.info("Running security.services.telnet") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No telnet server found" + else: + return False, f"Found telnet server running on port {', '.join(open_ports)}" + + def _security_services_smtp(self, config): + LOGGER.info("Running security.services.smtp") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No SMTP server found" + else: + return False, f"Found SMTP server running on port {', '.join(open_ports)}" + + def _security_services_http(self, config): + LOGGER.info("Running security.services.http") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No HTTP server found" + else: + return False, f"Found HTTP server running on port {', '.join(open_ports)}" + + def _security_services_pop(self, config): + LOGGER.info("Running security.services.pop") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No POP server found" + else: + return False, f"Found POP server running on port {', '.join(open_ports)}" + + def _security_services_imap(self, config): + LOGGER.info("Running security.services.imap") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No IMAP server found" + else: + return False, f"Found IMAP server running on port {', '.join(open_ports)}" + + def _security_services_snmpv3(self, config): + LOGGER.info("Running security.services.snmpv3") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No SNMP server found" + else: + return False, f"Found SNMP server running on port {', '.join(open_ports)}" + + def _security_services_vnc(self, config): + LOGGER.info("Running ntp.services.vnc") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No VNC server found" + else: + return False, f"Found VNC server running on port {', '.join(open_ports)}" + + def _security_services_tftp(self, config): + LOGGER.info("Running security.services.tftp") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No TFTP server found" + else: + return False, f"Found TFTP server running on port {', '.join(open_ports)}" + + def _ntp_network_ntp_server(self, config): + LOGGER.info("Running ntp.network.ntp_server") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No NTP server found" + else: + return False, f"Found NTP server running on port {', '.join(open_ports)}" + + def _security_ssh_version(self, config): + LOGGER.info("Running security.ssh.version") + + open_ports = self._check_results(config["ports"], config["services"]) + if len(open_ports) == 0: + return True, "No SSH server found" + else: + # Perform version check + for open_port, open_port_info in self._scan_results.items(): + if ((open_port == 22 or open_port_info["service"] == "ssh") and + open_port_info["state"] == "open"): + if config["version"] in open_port_info["version"]: + return True, f"SSH server found running {open_port_info['version']}" + else: + return False, f"SSH server found running {open_port_info['version']}" diff --git a/modules/test/ntp/conf/module_config.json b/modules/test/ntp/conf/module_config.json index a1a297f06..fbfb04874 100644 --- a/modules/test/ntp/conf/module_config.json +++ b/modules/test/ntp/conf/module_config.json @@ -14,13 +14,13 @@ "tests":[ { "name": "ntp.network.ntp_support", - "description": "Does the device request network time sync as client as per RFC 5905 - Network Time Protocol Version 4: Protocol and Algorithms Specification", + "test_description": "Does the device request network time sync as client as per RFC 5905 - Network Time Protocol Version 4: Protocol and Algorithms Specification", "expected_behavior": "The device sends an NTPv4 request to the configured NTP server.", "required_result": "Required" }, { "name": "ntp.network.ntp_dhcp", - "description": "Accept NTP address over DHCP", + "test_description": "Accept NTP address over DHCP", "expected_behavior": "Device can accept NTP server address, provided by the DHCP server (DHCP OFFER PACKET)", "required_result": "Roadmap" } diff --git a/modules/test/protocol/bin/start_test_module b/modules/test/protocol/bin/start_test_module new file mode 100644 index 000000000..a0754836c --- /dev/null +++ b/modules/test/protocol/bin/start_test_module @@ -0,0 +1,53 @@ +#!/bin/bash + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Setup and start the connection test module + +# Define where the python source files are located +PYTHON_SRC_DIR=/testrun/python/src + +# Fetch module name +MODULE_NAME=$1 + +# Default interface should be veth0 for all containers +DEFAULT_IFACE=veth0 + +# Allow a user to define an interface by passing it into this script +DEFINED_IFACE=$2 + +# Select which interace to use +if [[ -z $DEFINED_IFACE || "$DEFINED_IFACE" == "null" ]] +then + echo "No interface defined, defaulting to veth0" + INTF=$DEFAULT_IFACE +else + INTF=$DEFINED_IFACE +fi + +# Create and set permissions on the log files +LOG_FILE=/runtime/output/$MODULE_NAME.log +RESULT_FILE=/runtime/output/$MODULE_NAME-result.json +touch $LOG_FILE +touch $RESULT_FILE +chown $HOST_USER $LOG_FILE +chown $HOST_USER $RESULT_FILE + +# Run the python script that will execute the tests for this module +# -u flag allows python print statements +# to be logged by docker by running unbuffered +python3 -u $PYTHON_SRC_DIR/run.py "-m $MODULE_NAME" + +echo Module has finished \ No newline at end of file diff --git a/modules/test/protocol/conf/module_config.json b/modules/test/protocol/conf/module_config.json new file mode 100644 index 000000000..0fa83afb8 --- /dev/null +++ b/modules/test/protocol/conf/module_config.json @@ -0,0 +1,56 @@ +{ + "config": { + "enabled": false, + "meta": { + "name": "protocol", + "display_name": "Protocol", + "description": "Protocol tests" + }, + "network": true, + "docker": { + "depends_on": "base", + "enable_container": true, + "timeout": 300 + }, + "tests":[ + { + "name": "protocol.valid_bacnet", + "test_description": "Can valid BACnet traffic be seen", + "expected_behavior": "BACnet traffic can be seen on the network and packets are valid and not malformed", + "required_result": "Required" + }, + { + "name": "protocol.valid_modbus", + "test_description": "Can valid Modbus traffic be seen", + "expected_behavior": "Any Modbus functionality works as expected and valid modbus traffic can be observed", + "required_result": "Required", + "config":{ + "port": 502, + "device_id": 1, + "registers":{ + "holding":{ + "enabled": true, + "address_start": 0, + "count": 5 + }, + "input":{ + "enabled": true, + "address_start": 0, + "count": 5 + }, + "coil":{ + "enabled": true, + "address_start": 0, + "count": 1 + }, + "discrete":{ + "enabled": true, + "address_start": 0, + "count": 1 + } + } + } + } + ] + } +} \ No newline at end of file diff --git a/modules/test/protocol/protocol.Dockerfile b/modules/test/protocol/protocol.Dockerfile new file mode 100644 index 000000000..abfbc16b0 --- /dev/null +++ b/modules/test/protocol/protocol.Dockerfile @@ -0,0 +1,34 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Image name: test-run/protocol-test +FROM test-run/base-test:latest + +ARG MODULE_NAME=protocol +ARG MODULE_DIR=modules/test/$MODULE_NAME + +#Load the requirements file +COPY $MODULE_DIR/python/requirements.txt /testrun/python + +#Install all python requirements for the module +RUN pip3 install -r /testrun/python/requirements.txt + +# Copy over all configuration files +COPY $MODULE_DIR/conf /testrun/conf + +# Copy over all binary files +COPY $MODULE_DIR/bin /testrun/bin + +# Copy over all python files +COPY $MODULE_DIR/python /testrun/python \ No newline at end of file diff --git a/modules/test/protocol/python/requirements.txt b/modules/test/protocol/python/requirements.txt new file mode 100644 index 000000000..57917735d --- /dev/null +++ b/modules/test/protocol/python/requirements.txt @@ -0,0 +1,7 @@ +# Required for BACnet protocol tests +netifaces +BAC0 +pytz + +# Required for Modbus protocol tests +pymodbus \ No newline at end of file diff --git a/modules/test/protocol/python/src/protocol_bacnet.py b/modules/test/protocol/python/src/protocol_bacnet.py new file mode 100644 index 000000000..557dcdfb9 --- /dev/null +++ b/modules/test/protocol/python/src/protocol_bacnet.py @@ -0,0 +1,71 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Module run all the BACnet related methods for testing""" + +import BAC0 +import logging + +LOGGER = None +BAC0_LOG = '/root/.BAC0/BAC0.log' + +class BACnet(): + """BACnet Test module""" + + def __init__(self, log): + # Set the log + global LOGGER + LOGGER = log + + # Setup the BAC0 Log + BAC0.log_level(log_file=logging.DEBUG, stdout=logging.INFO, stderr=logging.CRITICAL) + + self.devices = [] + + def discover(self, local_ip=None): + LOGGER.info("Performing BACnet discovery...") + bacnet = BAC0.lite(local_ip) + LOGGER.info("Local BACnet object: " + str(bacnet)) + try: + bacnet.discover(global_broadcast=True) + except Exception as e: + LOGGER.error(e) + LOGGER.info("BACnet discovery complete") + with open(BAC0_LOG,'r',encoding='utf-8') as f: + bac0_log = f.read() + LOGGER.info("BAC0 Log:\n" + bac0_log) + self.devices = bacnet.devices + + # Check if the device being tested is in the discovered devices list + def validate_device(self, local_ip, device_ip): + result = None + LOGGER.info("Validating BACnet device: " + device_ip) + self.discover(local_ip + '/24') + LOGGER.info("BACnet Devices Found: " + str(len(self.devices))) + if len(self.devices) > 0: + # Load a fail result initially and pass only + # if we can validate it's the right device responding + result = False, ( + f'Could not confirm discovered BACnet device is the ' + + 'same as device being tested') + for device in self.devices: + name, vendor, address, device_id = device + LOGGER.info("Checking Device: " + str(device)) + if device_ip in address: + result = True, 'Device IP matches discovered device' + break + else: + result = None, 'BACnet discovery could not resolve any devices' + if result is not None: + LOGGER.info(result[1]) + return result diff --git a/modules/test/protocol/python/src/protocol_modbus.py b/modules/test/protocol/python/src/protocol_modbus.py new file mode 100644 index 000000000..6204a0e41 --- /dev/null +++ b/modules/test/protocol/python/src/protocol_modbus.py @@ -0,0 +1,272 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Module run all the Modbus related methods for testing""" + +from pymodbus.client import ModbusTcpClient as ModbusClient +from pymodbus.exceptions import ModbusIOException + +DEFAULT_MODBUS_PORT = 502 +DEFAULT_DEVICE_ID = 1 +DEFAULT_REG_START = 0 +DEFAULT_REG_COUNT = 1 +LOGGER = None + + +class Modbus(): + """Modbus Test module""" + + def __init__(self, log, device_ip, config): + # Setup the log + global LOGGER + LOGGER = log + + # Setup modbus addressing + self._port = config['port'] if 'port' in config else DEFAULT_MODBUS_PORT + self._device_id = config[ + 'device_id'] if 'device_id' in config else DEFAULT_DEVICE_ID + + # Setup default register states + self._holding_reg_enabled = True + self._input_reg_enabled = True + self._coil_enabled = True + self._discrete_input_enabled = True + self._holding_reg_start = DEFAULT_REG_START + self._holding_reg_count = DEFAULT_REG_COUNT + self._input_reg_start = DEFAULT_REG_START + self._input_reg_count = DEFAULT_REG_COUNT + self._coil_reg_start = DEFAULT_REG_START + self._coil_reg_count = DEFAULT_REG_COUNT + self._discrete_input_reg_start = DEFAULT_REG_START + self._discrete_input_reg_count = DEFAULT_REG_COUNT + + LOGGER.info('Config: ' + str(config)) + # Extract all register information + if 'registers' in config: + + # Extract holding register information + if 'holding' in config['registers']: + if ('enabled' in config['registers']['holding'] + and config['registers']['holding']['enabled']) or ( + 'enabled' not in config['registers']['holding']): + self._holding_reg_start = config['registers']['holding'].get( + 'address_start', DEFAULT_REG_START) + self._holding_reg_count = config['registers']['holding'].get( + 'count', DEFAULT_REG_COUNT) + else: + self._holding_reg_enabled = False + + # Extract input register information + if 'input' in config['registers']: + if ('enabled' in config['registers']['input'] + and config['registers']['input']['enabled']) or ( + 'enabled' not in config['registers']['input']): + self._input_reg_start = config['registers']['input'].get( + 'address_start', DEFAULT_REG_START) + self._input_reg_count = config['registers']['input'].get( + 'count', DEFAULT_REG_COUNT) + else: + self._input_reg_enabled = False + + # Extract coil register information + if 'coil' in config['registers']: + if ('enabled' in config['registers']['coil'] + and config['registers']['coil']['enabled']) or ( + 'enabled' not in config['registers']['coil']): + self._coil_reg_start = config['registers']['coil'].get( + 'address_start', DEFAULT_REG_START) + self._coil_reg_count = config['registers']['coil'].get( + 'count', DEFAULT_REG_COUNT) + else: + self._coil_enabled = False + + # Extract discrete register information + if 'discrete' in config['registers']: + if ('enabled' in config['registers']['discrete'] + and config['registers']['discrete']['enabled']) or ( + 'enabled' not in config['registers']['discrete']): + self._discrete_input_reg_start = config['registers']['discrete'].get( + 'address_start', DEFAULT_REG_START) + self._discrete_input_reg_count = config['registers']['discrete'].get( + 'count', DEFAULT_REG_COUNT) + else: + self._discrete_input_enabled = False + + # Initialize the modbus client + self.client = ModbusClient(device_ip, self._port) + + # Connections created from this method are simple socket connections + # and aren't indicative of valid modbus + def connect(self): + connection = None + try: + LOGGER.info(f'Attempting modbus connection to: {str()}') + connection = self.client.connect() + if connection: + LOGGER.info('Connected to Modbus device') + else: + LOGGER.info('Failed to connect to Modbus device') + except ModbusIOException as e: + LOGGER.error('Modbus Connection Failed:', e) + return connection + + # Read a range of holding registers + def read_holding_registers(self, + address=DEFAULT_REG_START, + count=DEFAULT_REG_COUNT, + device_id=DEFAULT_DEVICE_ID): + registers = None + LOGGER.info(f'Reading holding registers: {address}:{count}') + try: + response = self.client.read_holding_registers(address, + count, + slave=device_id) + if response.isError(): + LOGGER.error(f'Failed to read holding registers: {address}:{count}') + LOGGER.error('Read Response: ' + str(response)) + else: + registers = response.registers + LOGGER.info(f'Holding registers read: {str(registers)}') + except ModbusIOException as e: + LOGGER.error('Error reading holding registers:' + e) + return registers + + # Read a range of input registers + def read_input_registers(self, + address=DEFAULT_REG_START, + count=DEFAULT_REG_COUNT, + device_id=DEFAULT_DEVICE_ID): + registers = None + LOGGER.info(f'Reading input registers: {address}:{count}') + try: + response = self.client.read_input_registers(address, + count, + slave=device_id) + if response.isError(): + LOGGER.error(f'Failed to read input registers: {address}:{count}') + LOGGER.error('Read Response: ' + str(response)) + else: + registers = response.registers + LOGGER.info(f'Input registers read: {str(registers)}') + except ModbusIOException as e: + LOGGER.error('Error reading input registers:' + e) + return registers + + # Read a range of input registers + def read_coils(self, + address=DEFAULT_REG_START, + count=DEFAULT_REG_COUNT, + device_id=DEFAULT_DEVICE_ID): + coils = None + LOGGER.info(f'Reading coil registers: {address}:{count}') + try: + response = self.client.read_coils(address, count, slave=device_id) + if response.isError(): + LOGGER.error(f'Failed to read coil registers: {address}:{count}') + LOGGER.error('Read Response: ' + str(response)) + else: + coils = response.bits + LOGGER.info(f'Coil registers read: {str(coils)}') + except ModbusIOException as e: + LOGGER.error('Error reading coil registers:' + e) + return coils + + # Read a range of input registers + def read_discrete_inputs(self, + address=DEFAULT_REG_START, + count=DEFAULT_REG_COUNT, + device_id=DEFAULT_DEVICE_ID): + inputs = None + LOGGER.info(f'Reading discrete inputs: {address}:{count}') + try: + response = self.client.read_discrete_inputs(address, + count, + slave=device_id) + if response.isError(): + LOGGER.error(f'Failed to read discrete inputs: {address}:{count}') + LOGGER.error('Read Response: ' + str(response)) + else: + inputs = response.bits + LOGGER.info(f'Discrete inputs read: {str(inputs)}') + except ModbusIOException as e: + LOGGER.error('Error reading discrete inputs:' + e) + return inputs + + # Check if we can make a modbus connection and read various registers + # We don't care what the values in the registers are, just that + # we can read them since we will not have an expectation + # of the contents of the values + def validate_device(self): + result = None + compliant = None + details = '' + LOGGER.info('Validating Modbus device') + connection = self.connect() + if connection: + details = f'Established connection to modbus port: {self._port}' + + # Validate if the device supports holding registers and can be read + holding_reg = self.read_holding_registers(self._holding_reg_start, + self._holding_reg_count, + self._device_id) + if holding_reg: + details += ('\nHolding registers succesfully read: ' + + f'{self._holding_reg_start}:{self._holding_reg_count}') + else: + details += ('\nHolding registers could not be read: ' + + f'{self._holding_reg_start}:{self._holding_reg_count}') + + # Validate if the device supports input registers and can be read + input_reg = self.read_input_registers(self._input_reg_start, + self._input_reg_count, + self._device_id) + if input_reg: + details += ('\nInput registers succesfully read: ' + + f'{self._input_reg_start}:{self._input_reg_count}') + else: + details += ('\nInput registers could not be read: ' + + f'{self._input_reg_start}:{self._input_reg_count}') + + # Validate if the device supports coils and can be read + coils = self.read_coils(self._coil_reg_start, self._coil_reg_count, + self._device_id) + if coils: + details += ('\nCoil registers succesfully read: ' + + f'{self._coil_reg_start}:{self._coil_reg_count}') + else: + details += ('\nCoil registers could not be read: ' + + f'{self._coil_reg_start}:{self._coil_reg_count}') + + # Validate if the device supports discrete inputs and can be read + discrete_inputs = self.read_discrete_inputs( + self._discrete_input_reg_start, self._discrete_input_reg_count, + self._device_id) + if discrete_inputs: + details += ( + '\nDiscrete inputs succesfully read: ' + + f'{self._discrete_input_reg_start}:{self._discrete_input_reg_count}' + ) + else: + details += ( + '\nDiscrete inputs could not be read: ' + + f'{self._discrete_input_reg_start}:{self._discrete_input_reg_count}' + ) + + # Since we can't know what data types the device supports + # we'll pass if any of the supported data types are succesfully read + compliant = holding_reg or input_reg or coils or discrete_inputs + else: + compliant = False + details = 'Failed to establish Modbus connection to device' + result = compliant, details + return result diff --git a/modules/test/protocol/python/src/protocol_module.py b/modules/test/protocol/python/src/protocol_module.py new file mode 100644 index 000000000..b3233b6ae --- /dev/null +++ b/modules/test/protocol/python/src/protocol_module.py @@ -0,0 +1,63 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Protocol test module""" +from test_module import TestModule +import netifaces +from protocol_bacnet import BACnet +from protocol_modbus import Modbus + +LOG_NAME = 'test_protocol' +LOGGER = None + + +class ProtocolModule(TestModule): + """Protocol Test module""" + + def __init__(self, module): + super().__init__(module_name=module, log_name=LOG_NAME) + global LOGGER + LOGGER = self._get_logger() + self._bacnet = BACnet(LOGGER) + + def _protocol_valid_bacnet(self): + LOGGER.info('Running protocol.valid_bacnet') + result = None + interface_name = 'veth0' + + # Resolve the appropriate IP for BACnet comms + local_address = self.get_local_ip(interface_name) + if local_address: + result = self._bacnet.validate_device(local_address, + self._device_ipv4_addr) + else: + result = None, 'Could not resolve test container IP for BACnet discovery' + return result + + def _protocol_valid_modbus(self, config): + LOGGER.info('Running protocol.valid_modbus') + # Extract basic device connection information + modbus = Modbus(log=LOGGER, device_ip=self._device_ipv4_addr, config=config) + return modbus.validate_device() + + def get_local_ip(self, interface_name): + try: + addresses = netifaces.ifaddresses(interface_name) + local_address = addresses[netifaces.AF_INET][0]['addr'] + if local_address: + LOGGER.info(f'IP address of {interface_name}: {local_address}') + else: + LOGGER.info(f'Unable to retrieve IP address for {interface_name}') + return local_address + except (KeyError, IndexError): + return None diff --git a/modules/test/protocol/python/src/run.py b/modules/test/protocol/python/src/run.py new file mode 100644 index 000000000..d47c81cb6 --- /dev/null +++ b/modules/test/protocol/python/src/run.py @@ -0,0 +1,68 @@ +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Run Baseline module""" +import argparse +import signal +import sys +import logger +from protocol_module import ProtocolModule + +LOGGER = logger.get_logger('test_module') +RUNTIME = 1500 + + +class ProtocolModuleRunner: + """An example runner class for test modules.""" + + def __init__(self, module): + + signal.signal(signal.SIGINT, self._handler) + signal.signal(signal.SIGTERM, self._handler) + signal.signal(signal.SIGABRT, self._handler) + signal.signal(signal.SIGQUIT, self._handler) + + LOGGER.info('Starting Protocol Module') + + self._test_module = ProtocolModule(module) + self._test_module.run_tests() + + def _handler(self, signum): + LOGGER.debug('SigtermEnum: ' + str(signal.SIGTERM)) + LOGGER.debug('Exit signal received: ' + str(signum)) + if signum in (2, signal.SIGTERM): + LOGGER.info('Exit signal received. Stopping test module...') + LOGGER.info('Test module stopped') + sys.exit(1) + + +def run(): + parser = argparse.ArgumentParser( + description='Protocol Module Help', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + + parser.add_argument( + '-m', + '--module', + help='Define the module name to be used to create the log file') + + args = parser.parse_args() + + # For some reason passing in the args from bash adds an extra + # space before the argument so we'll just strip out extra space + ProtocolModuleRunner(args.module.strip()) + + +if __name__ == '__main__': + run() diff --git a/modules/test/tls/conf/module_config.json b/modules/test/tls/conf/module_config.json index 7f0305d19..194d1198c 100644 --- a/modules/test/tls/conf/module_config.json +++ b/modules/test/tls/conf/module_config.json @@ -14,25 +14,25 @@ "tests":[ { "name": "security.tls.v1_2_server", - "description": "Check the device web server TLS 1.2 & certificate is valid", + "test_description": "Check the device web server TLS 1.2 & certificate is valid", "expected_behavior": "TLS 1.2 certificate is issued to the web browser client when accessed", "required_result": "Required" }, { "name": "security.tls.v1_3_server", - "description": "Check the device web server TLS 1.3 & certificate is valid", + "test_description": "Check the device web server TLS 1.3 & certificate is valid", "expected_behavior": "TLS 1.3 certificate is issued to the web browser client when accessed", "required_result": "Recommended" }, { "name": "security.tls.v1_2_client", - "description": "Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS)", + "test_description": "Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS)", "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.2 and support for ECDH and ECDSA ciphers", "required_result": "Required" }, { "name": "security.tls.v1_3_client", - "description": "Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS)", + "test_description": "Device uses TLS with connection to an external service on port 443 (or any other port which could be running the webserver-HTTPS)", "expected_behavior": "The packet indicates a TLS connection with at least TLS 1.3", "required_result": "Recommended" } diff --git a/modules/test/tls/python/src/tls_module.py b/modules/test/tls/python/src/tls_module.py index d58163266..970152768 100644 --- a/modules/test/tls/python/src/tls_module.py +++ b/modules/test/tls/python/src/tls_module.py @@ -43,7 +43,7 @@ def _security_tls_v1_2_server(self): tls_1_3_results) else: LOGGER.error('Could not resolve device IP address. Skipping') - return None, 'Could not resolve device IP address. Skipping' + return None, 'Could not resolve device IP address' def _security_tls_v1_3_server(self): LOGGER.info('Running security.tls.v1_3_server') @@ -54,7 +54,7 @@ def _security_tls_v1_3_server(self): tls_version='1.3') else: LOGGER.error('Could not resolve device IP address. Skipping') - return None, 'Could not resolve device IP address. Skipping' + return None, 'Could not resolve device IP address' def _security_tls_v1_2_client(self): LOGGER.info('Running security.tls.v1_2_client') @@ -64,7 +64,7 @@ def _security_tls_v1_2_client(self): return self._validate_tls_client(self._device_ipv4_addr, '1.2') else: LOGGER.error('Could not resolve device IP address. Skipping') - return None, 'Could not resolve device IP address. Skipping' + return None, 'Could not resolve device IP address' def _security_tls_v1_3_client(self): LOGGER.info('Running security.tls.v1_3_client') @@ -74,7 +74,7 @@ def _security_tls_v1_3_client(self): return self._validate_tls_client(self._device_ipv4_addr, '1.3') else: LOGGER.error('Could not resolve device IP address. Skipping') - return None, 'Could not resolve device IP address. Skipping' + return None, 'Could not resolve device IP address' def _validate_tls_client(self, client_ip, tls_version): monitor_result = self._tls_util.validate_tls_client( @@ -97,7 +97,7 @@ def _validate_tls_client(self, client_ip, tls_version): elif monitor_result[0] and startup_result[0] is None: result = True, monitor_result[1] elif startup_result[0] and monitor_result[0] is None: - result = True, monitor_result[1] + result = True, startup_result[1] else: result = None, startup_result[1] return result diff --git a/modules/test/tls/python/src/tls_util.py b/modules/test/tls/python/src/tls_util.py index c83c131af..2cb7011cd 100644 --- a/modules/test/tls/python/src/tls_util.py +++ b/modules/test/tls/python/src/tls_util.py @@ -185,29 +185,29 @@ def validate_signature(self, host): def process_tls_server_results(self, tls_1_2_results, tls_1_3_results): results = '' if tls_1_2_results[0] is None and tls_1_3_results[0]: - results = True, 'TLS 1.3 validated:\n' + tls_1_3_results[1] + results = True, 'TLS 1.3 validated: ' + tls_1_3_results[1] elif tls_1_3_results[0] is None and tls_1_2_results[0]: - results = True, 'TLS 1.2 validated:\n' + tls_1_2_results[1] + results = True, 'TLS 1.2 validated: ' + tls_1_2_results[1] elif tls_1_2_results[0] and tls_1_3_results[0]: - description = 'TLS 1.2 validated:\n' + tls_1_2_results[1] - description += '\nTLS 1.3 validated:\n' + tls_1_3_results[1] + description = 'TLS 1.2 validated: ' + tls_1_2_results[1] + '. ' + description += '\nTLS 1.3 validated: ' + tls_1_3_results[1] + '. ' results = True, description elif tls_1_2_results[0] and not tls_1_3_results[0]: - description = 'TLS 1.2 validated:\n' + tls_1_2_results[1] - description += '\nTLS 1.3 not validated:\n' + tls_1_3_results[1] + description = 'TLS 1.2 validated: ' + tls_1_2_results[1] + '. ' + description += '\nTLS 1.3 not validated: ' + tls_1_3_results[1] + '. ' results = True, description elif tls_1_3_results[0] and not tls_1_2_results[0]: - description = 'TLS 1.2 not validated:\n' + tls_1_2_results[1] - description += '\nTLS 1.3 validated:\n' + tls_1_3_results[1] + description = 'TLS 1.2 not validated: ' + tls_1_2_results[1] + '. ' + description += 'TLS 1.3 validated: ' + tls_1_3_results[1] + '. ' results = True, description elif not tls_1_3_results[0] and not tls_1_2_results[0] and tls_1_2_results[ 0] is not None and tls_1_3_results is not None: - description = 'TLS 1.2 not validated:\n' + tls_1_2_results[1] - description += '\nTLS 1.3 not validated:\n' + tls_1_3_results[1] + description = 'TLS 1.2 not validated:' + tls_1_2_results[1] + '. ' + description += 'TLS 1.3 not validated: ' + tls_1_3_results[1] + '. ' results = False, description else: - description = 'TLS 1.2 not validated:\n' + tls_1_2_results[1] - description += '\nTLS 1.3 not validated:\n' + tls_1_3_results[1] + description = 'TLS 1.2 not validated: ' + tls_1_2_results[1] + '. ' + description += 'TLS 1.3 not validated: ' + tls_1_3_results[1] + '. ' results = None, description LOGGER.info('TLS 1.2 server test results: ' + str(results)) return results @@ -227,7 +227,7 @@ def validate_tls_server(self, host, tls_version): # Print the certificate information cert_text = crypto.dump_certificate(crypto.FILETYPE_TEXT, public_cert).decode() - LOGGER.info('Device Certificate:\n' + cert_text) + LOGGER.info('Device certificate:\n' + cert_text) # Validate the certificates time range tr_valid = self.verify_certificate_timerange(public_cert) @@ -243,7 +243,7 @@ def validate_tls_server(self, host, tls_version): cert_valid = tr_valid[0] and key_valid[0] and sig_valid[0] test_details = tr_valid[1] + '\n' + key_valid[1] + '\n' + sig_valid[1] LOGGER.info('Certificate validated: ' + str(cert_valid)) - LOGGER.info('Test Details:\n' + test_details) + LOGGER.info('Test details:\n' + test_details) return cert_valid, test_details else: LOGGER.info('Failed to resolve public certificate') diff --git a/modules/test/tls/tls.Dockerfile b/modules/test/tls/tls.Dockerfile index 92fa6028c..cedf9531b 100644 --- a/modules/test/tls/tls.Dockerfile +++ b/modules/test/tls/tls.Dockerfile @@ -40,9 +40,5 @@ RUN pip3 install -r /testrun/python/requirements.txt # Create a directory inside the container to store the root certificates RUN mkdir -p /testrun/root_certs -# Copy over all the local certificates for device signature -# checks if the folder exists -COPY $CERTS_DIR /testrun/root_certs - diff --git a/modules/ui/.editorconfig b/modules/ui/.editorconfig new file mode 100644 index 000000000..59d9a3a3e --- /dev/null +++ b/modules/ui/.editorconfig @@ -0,0 +1,16 @@ +# Editor configuration, see https://editorconfig.org +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 2 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/modules/ui/.gitignore b/modules/ui/.gitignore new file mode 100644 index 000000000..57fa6bf61 --- /dev/null +++ b/modules/ui/.gitignore @@ -0,0 +1,46 @@ +node_modules/ +.angular/ +dist/ + +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/modules/ui/angular.json b/modules/ui/angular.json new file mode 100644 index 000000000..f029e61e9 --- /dev/null +++ b/modules/ui/angular.json @@ -0,0 +1,104 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "test-run-ui": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "style": "scss" + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": [ + "zone.js" + ], + "tsConfig": "tsconfig.app.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.scss" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "1000kb", + "maximumError": "2000kb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "test-run-ui:build:production" + }, + "development": { + "browserTarget": "test-run-ui:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "test-run-ui:build" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "tsconfig.spec.json", + "inlineStyleLanguage": "scss", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.scss" + ], + "scripts": [] + } + } + } + } + } +} diff --git a/modules/ui/package-lock.json b/modules/ui/package-lock.json new file mode 100644 index 000000000..e07c4fd91 --- /dev/null +++ b/modules/ui/package-lock.json @@ -0,0 +1,12407 @@ +{ + "name": "test-run-ui", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "test-run-ui", + "version": "0.0.0", + "dependencies": { + "@angular/animations": "^16.1.0", + "@angular/cdk": "^16.1.4", + "@angular/common": "^16.1.0", + "@angular/compiler": "^16.1.0", + "@angular/core": "^16.1.0", + "@angular/forms": "^16.1.0", + "@angular/material": "^16.1.4", + "@angular/platform-browser": "^16.1.0", + "@angular/platform-browser-dynamic": "^16.1.0", + "@angular/router": "^16.1.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.13.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^16.1.3", + "@angular/cli": "~16.1.3", + "@angular/compiler-cli": "^16.1.0", + "@types/jasmine": "~4.3.0", + "jasmine-core": "~4.6.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.1.3" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", + "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1601.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1601.3.tgz", + "integrity": "sha512-HvW51cCEoIYe2mYqcmnm2RZiMMFbFn7iIdsjbCJe7etFhcG+Y3hGDZMh4IFSiQiss+pwPSYOvQY2zwGrndMgLw==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "16.1.3", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-16.1.3.tgz", + "integrity": "sha512-1scrdUdKRa9TkJ9jev/KRzFttbLUVACQvVRL0G67nUAdtJ/bQX8eui85axpCNPFihK4ReSW3R4lrgcVC2NUSoA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "2.2.1", + "@angular-devkit/architect": "0.1601.3", + "@angular-devkit/build-webpack": "0.1601.3", + "@angular-devkit/core": "16.1.3", + "@babel/core": "7.22.5", + "@babel/generator": "7.22.5", + "@babel/helper-annotate-as-pure": "7.22.5", + "@babel/helper-split-export-declaration": "7.22.5", + "@babel/plugin-proposal-async-generator-functions": "7.20.7", + "@babel/plugin-transform-async-to-generator": "7.22.5", + "@babel/plugin-transform-runtime": "7.22.5", + "@babel/preset-env": "7.22.5", + "@babel/runtime": "7.22.5", + "@babel/template": "7.22.5", + "@discoveryjs/json-ext": "0.5.7", + "@ngtools/webpack": "16.1.3", + "@vitejs/plugin-basic-ssl": "1.0.1", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.14", + "babel-loader": "9.1.2", + "babel-plugin-istanbul": "6.1.1", + "browserslist": "^4.21.5", + "cacache": "17.1.3", + "chokidar": "3.5.3", + "copy-webpack-plugin": "11.0.0", + "critters": "0.0.19", + "css-loader": "6.8.1", + "esbuild-wasm": "0.17.19", + "fast-glob": "3.2.12", + "https-proxy-agent": "5.0.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "karma-source-map-support": "1.4.0", + "less": "4.1.3", + "less-loader": "11.1.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.2.1", + "magic-string": "0.30.0", + "mini-css-extract-plugin": "2.7.6", + "mrmime": "1.0.1", + "open": "8.4.2", + "ora": "5.4.1", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "2.3.1", + "piscina": "3.2.0", + "postcss": "8.4.24", + "postcss-loader": "7.3.2", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.63.2", + "sass-loader": "13.3.1", + "semver": "7.5.3", + "source-map-loader": "4.0.1", + "source-map-support": "0.5.21", + "terser": "5.17.7", + "text-table": "0.2.0", + "tree-kill": "1.2.2", + "tslib": "2.5.3", + "vite": "4.3.9", + "webpack": "5.86.0", + "webpack-dev-middleware": "6.1.1", + "webpack-dev-server": "4.15.0", + "webpack-merge": "5.9.0", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.17.19" + }, + "peerDependencies": { + "@angular/compiler-cli": "^16.0.0", + "@angular/localize": "^16.0.0", + "@angular/platform-server": "^16.0.0", + "@angular/service-worker": "^16.0.0", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^16.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=4.9.3 <5.2" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/inquirer": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", + "integrity": "sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/tslib": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", + "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==", + "dev": true + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1601.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1601.3.tgz", + "integrity": "sha512-744+72vi/Vx010VxizGgilhpnDCOG29qyhMmu7BkUhtpq8E8eQn2HU3nPpxAqrg3bKVAwD7v3F111MVIhub8kA==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1601.3", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, + "node_modules/@angular-devkit/core": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.3.tgz", + "integrity": "sha512-cFhNdJHumNMZGD3NYxOtNuMGRQXeDnKbwvK+IJmKAttXt8na6EvURR/ZxZOI7rl/YRVX+vcNSdtXz3hE6g+Isw==", + "dev": true, + "dependencies": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^3.5.2" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.1.3.tgz", + "integrity": "sha512-hWEuQnfQOgcSs4YX6iF4QR/34ROeSPaMi7lQOYg33hStg+pnk/JDdIU0f2nrIIz3t0jqAj+5VXVLBJvOCd84vg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "16.1.3", + "jsonc-parser": "3.2.0", + "magic-string": "0.30.0", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/animations": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.1.3.tgz", + "integrity": "sha512-ET6ahrlbOyTYXOTouKs2VJxx0CMTrYkfz0HfI6IHnSKBC6wguDxXYnamMouHgrCkDDEB5qClfGHyS9se0AOX4w==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "16.1.3" + } + }, + "node_modules/@angular/cdk": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-16.1.4.tgz", + "integrity": "sha512-05m0+NoAkV5O15GUEX2GQLySe8iC6P0GXVqUjLipdGmZ2/pNndJ/DGbqkX8dAAo/Z3ss2TEyRNYMOJdLIjV5vw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "optionalDependencies": { + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@angular/common": "^16.0.0 || ^17.0.0", + "@angular/core": "^16.0.0 || ^17.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/cli": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.1.3.tgz", + "integrity": "sha512-D0gU12z/N2oJ+s6pggAnWYrTUZ+2duGb3Y5oUyClsubz7JWpAwHjSZpb8exPUrgYhr+qIEMGO685y1JazJQ2tA==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1601.3", + "@angular-devkit/core": "16.1.3", + "@angular-devkit/schematics": "16.1.3", + "@schematics/angular": "16.1.3", + "@yarnpkg/lockfile": "1.1.0", + "ansi-colors": "4.1.3", + "ini": "4.1.1", + "inquirer": "8.2.4", + "jsonc-parser": "3.2.0", + "npm-package-arg": "10.1.0", + "npm-pick-manifest": "8.0.1", + "open": "8.4.2", + "ora": "5.4.1", + "pacote": "15.2.0", + "resolve": "1.22.2", + "semver": "7.5.3", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@angular/cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@angular/cli/node_modules/cli-width": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-3.0.0.tgz", + "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@angular/cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@angular/cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@angular/cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@angular/cli/node_modules/inquirer": { + "version": "8.2.4", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.4.tgz", + "integrity": "sha512-nn4F01dxU8VeKfq192IjLsxu0/OmMZ4Lg3xKAns148rCaXP6ntAoEkVYZThWjwON8AlzdZZi6oqnhNbxUG9hVg==", + "dev": true, + "dependencies": { + "ansi-escapes": "^4.2.1", + "chalk": "^4.1.1", + "cli-cursor": "^3.1.0", + "cli-width": "^3.0.0", + "external-editor": "^3.0.3", + "figures": "^3.0.0", + "lodash": "^4.17.21", + "mute-stream": "0.0.8", + "ora": "^5.4.1", + "run-async": "^2.4.0", + "rxjs": "^7.5.5", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0", + "through": "^2.3.6", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@angular/cli/node_modules/mute-stream": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz", + "integrity": "sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA==", + "dev": true + }, + "node_modules/@angular/cli/node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/@angular/cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@angular/common": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.1.3.tgz", + "integrity": "sha512-ZzJ6EwQHUkiZYV0zH/UxyUYW5uxomsyk7tdtqZIxAR5m2ktYkQ5XlqgPjBO8voF54Rs5Ot43RkPCLesbZyJDsw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "16.1.3", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-16.1.3.tgz", + "integrity": "sha512-7Ckvssk9+s5xLyXvp72IwAw5vd/Osa3tR6oiQatdbw+O3XjLO04QycoGXwkp/fYVexGsjFyOn6QJ5n1F/PYPbQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/core": "16.1.3" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-16.1.3.tgz", + "integrity": "sha512-aUqnIV9rRTBNgiQRS0Gv6lhghaGj1vpVRyXgiE4VnTR9uBONSsGKMNALYBBhXRTSk2e0cvutt0ubLgmNpdyWyQ==", + "dev": true, + "dependencies": { + "@babel/core": "7.22.5", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^3.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.1.2", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/compiler": "16.1.3", + "typescript": ">=4.9.3 <5.2" + } + }, + "node_modules/@angular/core": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-16.1.3.tgz", + "integrity": "sha512-yhRo9hVS8KhfcEgzciWuRWF4Pnnko98bmSJTqd7u8Kys6z3Uj0qgXMssXHIPUALe3mQKjVkdSZPLIZ9/CaVn/Q==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.13.0" + } + }, + "node_modules/@angular/forms": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-16.1.3.tgz", + "integrity": "sha512-9tJHgoi/Jmeo30zfnReVZWFcd1WthR+QwYUNwPev+ys58u1mB0cDGORvROySmC2YUyXFSpXt8sxwyWCkYvaV2w==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "16.1.3", + "@angular/core": "16.1.3", + "@angular/platform-browser": "16.1.3", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/material": { + "version": "16.1.4", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-16.1.4.tgz", + "integrity": "sha512-1SKWB14J+IorRL6uzq4a9cBLpVOMONTzso05LoVLGKrmtMCL5cRYLM/otT0IjY+oqG/fnTpsYDwV7E6n7AljeA==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/auto-init": "15.0.0-canary.b994146f6.0", + "@material/banner": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/button": "15.0.0-canary.b994146f6.0", + "@material/card": "15.0.0-canary.b994146f6.0", + "@material/checkbox": "15.0.0-canary.b994146f6.0", + "@material/chips": "15.0.0-canary.b994146f6.0", + "@material/circular-progress": "15.0.0-canary.b994146f6.0", + "@material/data-table": "15.0.0-canary.b994146f6.0", + "@material/density": "15.0.0-canary.b994146f6.0", + "@material/dialog": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/drawer": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/fab": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/floating-label": "15.0.0-canary.b994146f6.0", + "@material/form-field": "15.0.0-canary.b994146f6.0", + "@material/icon-button": "15.0.0-canary.b994146f6.0", + "@material/image-list": "15.0.0-canary.b994146f6.0", + "@material/layout-grid": "15.0.0-canary.b994146f6.0", + "@material/line-ripple": "15.0.0-canary.b994146f6.0", + "@material/linear-progress": "15.0.0-canary.b994146f6.0", + "@material/list": "15.0.0-canary.b994146f6.0", + "@material/menu": "15.0.0-canary.b994146f6.0", + "@material/menu-surface": "15.0.0-canary.b994146f6.0", + "@material/notched-outline": "15.0.0-canary.b994146f6.0", + "@material/radio": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/segmented-button": "15.0.0-canary.b994146f6.0", + "@material/select": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/slider": "15.0.0-canary.b994146f6.0", + "@material/snackbar": "15.0.0-canary.b994146f6.0", + "@material/switch": "15.0.0-canary.b994146f6.0", + "@material/tab": "15.0.0-canary.b994146f6.0", + "@material/tab-bar": "15.0.0-canary.b994146f6.0", + "@material/tab-indicator": "15.0.0-canary.b994146f6.0", + "@material/tab-scroller": "15.0.0-canary.b994146f6.0", + "@material/textfield": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tooltip": "15.0.0-canary.b994146f6.0", + "@material/top-app-bar": "15.0.0-canary.b994146f6.0", + "@material/touch-target": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/animations": "^16.0.0 || ^17.0.0", + "@angular/cdk": "16.1.4", + "@angular/common": "^16.0.0 || ^17.0.0", + "@angular/core": "^16.0.0 || ^17.0.0", + "@angular/forms": "^16.0.0 || ^17.0.0", + "@angular/platform-browser": "^16.0.0 || ^17.0.0", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-16.1.3.tgz", + "integrity": "sha512-qZA6Lua2fpBe+KD/QArY/4hilypSZFcTcJsPjZwIzo5pavXqYDI8BVghwh5dcZoUa56hVRDJjv+XW6kl8m9Tdw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/animations": "16.1.3", + "@angular/common": "16.1.3", + "@angular/core": "16.1.3" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-16.1.3.tgz", + "integrity": "sha512-UHxSWpPB5+FSv8zm8T+4ZikLqyy+VE6GlOLp/DdgEz77j81rz2C1pMqozwTnVbD16XbI4rhTp+RFY3C9ArWOtw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "16.1.3", + "@angular/compiler": "16.1.3", + "@angular/core": "16.1.3", + "@angular/platform-browser": "16.1.3" + } + }, + "node_modules/@angular/router": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-16.1.3.tgz", + "integrity": "sha512-bkn8cWGBKKZidDaP+R7g/S/6miSfH8iP24d2k86Awo+vaO+7G/5WWGfKJMKK8UNM/A5ueX6ugAZrMHpQ9e6Y4w==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0" + }, + "peerDependencies": { + "@angular/common": "16.1.3", + "@angular/core": "16.1.3", + "@angular/platform-browser": "16.1.3", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@assemblyscript/loader": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@assemblyscript/loader/-/loader-0.10.1.tgz", + "integrity": "sha512-H71nDOOL8Y7kWRLqf6Sums+01Q5msqBW2KhDUTemh1tvY04eSkSXrK0uj/4mmY0Xr16/3zyZmsrxN7CKuRbNRg==", + "dev": true + }, + "node_modules/@babel/code-frame": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.5.tgz", + "integrity": "sha512-Xmwn266vad+6DAqEB2A6V/CcZVp62BbwVmcOJc2RPuwih1kw02TjQvWVWlcKGbBPd+8/0V5DEkOcizRGYsspYQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.22.5.tgz", + "integrity": "sha512-4Jc/YuIaYqKnDDz892kPIledykKg12Aw1PYX5i/TY28anJtacvM1Rrr8wbieB9GfEJwlzqT0hUEao0CxEebiDA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.22.5.tgz", + "integrity": "sha512-SBuTAjg91A3eKOvD+bPEz3LlhHZRNu1nFOVts9lzDJTXshHTjII0BAtDS3Y2DAkdZdDKWVZGVwkDfc4Clxn1dg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helpers": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.22.5.tgz", + "integrity": "sha512-+lcUbnTRhd0jOewtFSedLyiPsD5tswKkbgcezOqqWFUVNEwoUTlpPOBmvhG7OXWLR4jMdv0czPGH5XbflnD1EA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5", + "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.5.tgz", + "integrity": "sha512-m1EP3lVOPptR+2DwD125gziZNcmoNSHGmJROKoy87loWUQyJaVXDgpmruWqDARZSmtYQ+Dl25okU8+qhVzuykw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.22.5.tgz", + "integrity": "sha512-Ji+ywpHeuqxB8WDxraCiqR0xfhYjiDE/e6k7FuIaANnoOFxAHskHChz4vA1mJC9Lbm01s1PVAGhQY4FUKSkGZw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.22.5.tgz", + "integrity": "sha512-xkb58MyOYIslxu3gKmVXmjTtUPvBU4odYzbiIQbWwLKIHCsx6UGZGX6F1IznMFVnDdirseUZopzN+ZRt8Xb33Q==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.5.tgz", + "integrity": "sha512-1VpEFOIbMRaXyDeUwUfmTIxExLwQ+zkW+Bh5zXpApA3oQedBx9v/updixWxnx/bZpKw7u8VxWjb/qWpIcmPq8A==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.4.0.tgz", + "integrity": "sha512-RnanLx5ETe6aybRi1cO/edaRH+bNYWaryCEmjDDYyNr4wnSzyOp8T0dWipmqVHKEY3AbVKUom50AKSlj1zmKbg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.17.7", + "@babel/helper-plugin-utils": "^7.16.7", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2", + "semver": "^6.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0-0" + } + }, + "node_modules/@babel/helper-define-polyfill-provider/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.5.tgz", + "integrity": "sha512-XGmhECfVA/5sAt+H+xpSg0mfrHq6FzNr9Oxh7PSEBBRUb/mL7Kz3NICXb194rCqAEdxkhPT1a88teizAFyvk8Q==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.22.5.tgz", + "integrity": "sha512-wtHSq6jMRE3uF2otvfuD3DIvVhOsSNshQl0Qrd7qC9oQJzHvOL4qQXlQn2916+CXGywIjpGuIkoyZRRxHPiNQQ==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.22.5.tgz", + "integrity": "sha512-aBiH1NKMG0H2cGZqspNvsaBe6wNGjbJjuLy29aU+eDZjSbbN53BaxlpB02xm9v34pLTZ1nIQPFYn2qMZoa5BQQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.22.5.tgz", + "integrity": "sha512-8Dl6+HD/cKifutF5qGd/8ZJi84QeAKh+CEe1sBzz8UayBBGg1dAIJrdHOcOM5b2MpzWL2yuotJTtGjETq0qjXg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.22.5.tgz", + "integrity": "sha512-+hGKDt/Ze8GFExiVHno/2dvG5IdstpzCq0y4Qc9OJ25D4q3pKfiIP/4Vp3/JvhDkLKsDK2api3q3fpIgiIF5bw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz", + "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.5.tgz", + "integrity": "sha512-cU0Sq1Rf4Z55fgz7haOakIyM7+x/uCFwXpLPaeRzfoUtAEAuUZjZvFPjL/rk5rW693dIgn2hng1W7xbT7lWT4g==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-wrap-function": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.22.5.tgz", + "integrity": "sha512-aLdNM5I3kdI/V9xGNyKSF3X/gTyMUBohTZ+/3QdQKAA9vxIiy12E+8E2HoOP1/DjeqU+g6as35QHJNMDDYpuCg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-member-expression-to-functions": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.22.5.tgz", + "integrity": "sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.5.tgz", + "integrity": "sha512-thqK5QFghPKWLhAV321lxF95yCg2K3Ob5yw+M3VHWfdia0IkPXUtoLH8x/6Fh486QUvzhb8YOWHChTVen2/PoQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.22.5.tgz", + "integrity": "sha512-mM4COjgZox8U+JcXQwPijIZLElkgEpO5rsERVDJTc2qfCDfERyob6k5WegS14SX18IIjv+XD+GrqNumY5JRCDw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.5.tgz", + "integrity": "sha512-aJXu+6lErq8ltp+JhkJUfk1MTGyuA4v7f3pA+BJ5HLfNC6nAQ0Cpi9uOquUj8Hehg0aUiHzWQbOVJGao6ztBAQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.22.5.tgz", + "integrity": "sha512-R3oB6xlIVKUnxNUxbmgq7pKjxpru24zlimpE8WK47fACIlM0II/Hm1RS8IaOI7NgCr6LNS+jl5l75m20npAziw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.22.5.tgz", + "integrity": "sha512-bYqLIBSEshYcYQyfks8ewYA8S30yaGSeRslcvKMvoUk6HHPySbxHq9YRi6ghhzEU+yhQv9bP/jXnygkStOcqZw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.22.5", + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.22.5.tgz", + "integrity": "sha512-pSXRmfE1vzcUIDFQcSGA5Mr+GxBV9oiRKDuDxXvWQQBCh8HoIjs/2DlDB7H8smac1IVrB9/xdXj2N3Wol9Cr+Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.5", + "@babel/traverse": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.22.5.tgz", + "integrity": "sha512-BSKlD1hgnedS5XRnGOljZawtag7H1yPfQp0tdNJCHoH6AZ+Pcm9VvkrK59/Yy593Ypg0zMxH2BxD1VPYUQ7UIw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.22.5", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.22.5.tgz", + "integrity": "sha512-DFZMC9LJUG9PLOclRC32G63UXwzqS2koQC8dkx+PLdmt1xSePYpbT/NbsrJy8Q/muXz7o/h/d4A7Fuyixm559Q==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.22.5.tgz", + "integrity": "sha512-NP1M5Rf+u2Gw9qfSO4ihjcTGW5zXTi36ITLd4/EoAcEhIZ0yjMqmftDNl3QC19CX7olhrjpyU454g/2W7X0jvQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.22.5.tgz", + "integrity": "sha512-31Bb65aZaUwqCbWMnZPduIZxCBngHFlzyN6Dq6KAJjtx+lx6ohKHubc61OomYi7XwVD4Ol0XCVz4h+pYFR048g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-proposal-async-generator-functions": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.20.7.tgz", + "integrity": "sha512-xMbiLsn/8RK7Wq7VeVytytS2L6qE69bXPB10YCmMdDZbKF4okCqY74pI/jJQ/8U0b/F6NrT2+14b8/P9/3AMGA==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-plugin-utils": "^7.20.2", + "@babel/helper-remap-async-to-generator": "^7.18.9", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-proposal-unicode-property-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.18.6.tgz", + "integrity": "sha512-2BShG/d5yoZyXZfVePH91urL5wTG6ASZU9M4o03lKK8u8UW1y08OMttBSOADTcJrnPMpvDXRG3G8fyLh4ovs8w==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.22.5.tgz", + "integrity": "sha512-rdV97N7KqsRzeNGoWUOK6yUsWarLjE5Su/Snk9IYPU9CwkWHs4t+rTGOvffTR8XGkJMTAdLfO0xVnXm8wugIJg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.22.5.tgz", + "integrity": "sha512-KwvoWDeNKPETmozyFE0P2rOLqh39EoQHNjqizrI5B8Vt0ZNS7M56s7dAiAqbYfiAYOuIzIh96z3iR2ktgu3tEg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.22.5.tgz", + "integrity": "sha512-26lTNXoVRdAnsaDXPpvCNUq+OVWEVC6bx7Vvz9rC53F2bagUWW4u4ii2+h8Fejfh7RYqPxn+libeFBBck9muEw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.22.5.tgz", + "integrity": "sha512-gGOEvFzm3fWoyD5uZq7vVTD57pPJ3PczPUD/xCFGjzBpUosnklmXyKnGQbbbGs1NPNPskFex0j93yKbHt0cHyg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.22.5.tgz", + "integrity": "sha512-b1A8D8ZzE/VhNDoV1MSJTnpKkCG5bJo+19R4o4oy03zM7ws8yEMK755j61Dc3EyvdysbqH5BOOTquJ7ZX9C6vQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-remap-async-to-generator": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.22.5.tgz", + "integrity": "sha512-tdXZ2UdknEKQWKJP1KMNmuF5Lx3MymtMN/pvA+p/VEkhK8jVcQ1fzSy8KM9qRYhAf2/lV33hoMPKI/xaI9sADA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.22.5.tgz", + "integrity": "sha512-EcACl1i5fSQ6bt+YGuU/XGCeZKStLmyVGytWkpyhCLeQVA0eu6Wtiw92V+I1T/hnezUv7j74dA/Ro69gWcU+hg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.22.5.tgz", + "integrity": "sha512-nDkQ0NfkOhPTq8YCLiWNxp1+f9fCobEjCb0n8WdbNUBc4IB5V7P1QnX9IjpSoquKrXF5SKojHleVNs2vGeHCHQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.22.5.tgz", + "integrity": "sha512-SPToJ5eYZLxlnp1UzdARpOGeC2GbHvr9d/UV0EukuVx8atktg194oe+C5BqQ8jRTkgLRVOPYeXRSBg1IlMoVRA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.22.5.tgz", + "integrity": "sha512-2edQhLfibpWpsVBx2n/GKOz6JdGQvLruZQfGr9l1qes2KQaWswjBzhQF7UDUZMNaMMQeYnQzxwOMPsbYF7wqPQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.22.5.tgz", + "integrity": "sha512-4GHWBgRf0krxPX+AaPtgBAlTgTeZmqDynokHOX7aqqAB4tHs3U2Y02zH6ETFdLZGcg9UQSD1WCmkVrE9ErHeOg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/template": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.22.5.tgz", + "integrity": "sha512-GfqcFuGW8vnEqTUBM7UtPd5A4q797LTvvwKxXTgRsFjoqaJiEg9deBG6kWeQYkVEL569NpnmpC0Pkr/8BLKGnQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.22.5.tgz", + "integrity": "sha512-5/Yk9QxCQCl+sOIB1WelKnVRxTJDSAIxtJLL2/pqL14ZVlbH0fUQUZa/T5/UnQtBNgghR7mfB8ERBKyKPCi7Vw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.22.5.tgz", + "integrity": "sha512-dEnYD+9BBgld5VBXHnF/DbYGp3fqGMsyxKbtD1mDyIA7AkTSpKXFhCVuj/oQVOoALfBs77DudA0BE4d5mcpmqw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.22.5.tgz", + "integrity": "sha512-0MC3ppTB1AMxd8fXjSrbPa7LT9hrImt+/fcj+Pg5YMD7UQyWp/02+JWpdnCymmsXwIx5Z+sYn1bwCn4ZJNvhqQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.22.5.tgz", + "integrity": "sha512-vIpJFNM/FjZ4rh1myqIya9jXwrwwgFRHPjT3DkUA9ZLHuzox8jiXkOLvwm1H+PQIP3CqfC++WPKeuDi0Sjdj1g==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.22.5.tgz", + "integrity": "sha512-X4hhm7FRnPgd4nDA4b/5V280xCx6oL7Oob5+9qVS5C13Zq4bh1qq7LU0GgRU6b5dBWBvhGaXYVB4AcN6+ol6vg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.22.5.tgz", + "integrity": "sha512-3kxQjX1dU9uudwSshyLeEipvrLjBCVthCgeTp6CzE/9JYrlAIaeekVxRpCWsDDfYTfRZRoCeZatCQvwo+wvK8A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.22.5.tgz", + "integrity": "sha512-UIzQNMS0p0HHiQm3oelztj+ECwFnj+ZRV4KnguvlsD2of1whUeM6o7wGNj6oLwcDoAXQ8gEqfgC24D+VdIcevg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.22.5.tgz", + "integrity": "sha512-DuCRB7fu8MyTLbEQd1ew3R85nx/88yMoqo2uPSjevMj3yoN7CDM8jkgrY0wmVxfJZyJ/B9fE1iq7EQppWQmR5A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.22.5.tgz", + "integrity": "sha512-fTLj4D79M+mepcw3dgFBTIDYpbcB9Sm0bpm4ppXPaO+U+PKFFyV9MGRvS0gvGw62sd10kT5lRMKXAADb9pWy8g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.22.5.tgz", + "integrity": "sha512-MQQOUW1KL8X0cDWfbwYP+TbVbZm16QmQXJQ+vndPtH/BoO0lOKpVoEDMI7+PskYxH+IiE0tS8xZye0qr1lGzSA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.22.5.tgz", + "integrity": "sha512-RZEdkNtzzYCFl9SE9ATaUMTj2hqMb4StarOJLrZRbqqU4HSBE7UlBw9WBWQiDzrJZJdUWiMTVDI6Gv/8DPvfew==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.22.5.tgz", + "integrity": "sha512-R+PTfLTcYEmb1+kK7FNkhQ1gP4KgjpSO6HfH9+f8/yfp2Nt3ggBjiVpRwmwTlfqZLafYKJACy36yDXlEmI9HjQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.22.5.tgz", + "integrity": "sha512-B4pzOXj+ONRmuaQTg05b3y/4DuFz3WcCNAXPLb2Q0GT0TrGKGxNKV4jwsXts+StaM0LQczZbOpj8o1DLPDJIiA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.22.5.tgz", + "integrity": "sha512-emtEpoaTMsOs6Tzz+nbmcePl6AKVtS1yC4YNAeMun9U8YCsgadPNxnOPQ8GhHFB2qdx+LZu9LgoC0Lthuu05DQ==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.22.5.tgz", + "integrity": "sha512-+S6kzefN/E1vkSsKx8kmQuqeQsvCKCd1fraCM7zXm4SFoggI099Tr4G8U81+5gtMdUeMQ4ipdQffbKLX0/7dBQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.22.5.tgz", + "integrity": "sha512-AsF7K0Fx/cNKVyk3a+DW0JLo+Ua598/NxMRvxDnkpCIGFh43+h/v2xyhRUYf6oD8gE4QtL83C7zZVghMjHd+iw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.22.5.tgz", + "integrity": "sha512-6CF8g6z1dNYZ/VXok5uYkkBBICHZPiGEl7oDnAx2Mt1hlHVHOSIKWJaXHjQJA5VB43KZnXZDIexMchY4y2PGdA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.22.5.tgz", + "integrity": "sha512-NbslED1/6M+sXiwwtcAB/nieypGw02Ejf4KtDeMkCEpP6gWFMX1wI9WKYua+4oBneCCEmulOkRpwywypVZzs/g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.22.5.tgz", + "integrity": "sha512-Kk3lyDmEslH9DnvCDA1s1kkd3YWQITiBOHngOtDL9Pt6BZjzqb6hiOlb8VfjiiQJ2unmegBqZu0rx5RxJb5vmQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.22.5.tgz", + "integrity": "sha512-klXqyaT9trSjIUrcsYIfETAzmOEZL3cBYqOYLJxBHfMFFggmXOv+NYSX/Jbs9mzMVESw/WycLFPRx8ba/b2Ipw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-replace-supers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.22.5.tgz", + "integrity": "sha512-pH8orJahy+hzZje5b8e2QIlBWQvGpelS76C63Z+jhZKsmzfNaPQ+LaW6dcJ9bxTpo1mtXbgHwy765Ro3jftmUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.22.5.tgz", + "integrity": "sha512-AconbMKOMkyG+xCng2JogMCDcqW8wedQAqpVIL4cOSescZ7+iW8utC6YDZLMCSUIReEA733gzRSaOSXMAt/4WQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.22.5.tgz", + "integrity": "sha512-AVkFUBurORBREOmHRKo06FjHYgjrabpdqRSwq6+C7R5iTCZOsM4QbcB27St0a4U6fffyAOqh3s/qEfybAhfivg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.22.5.tgz", + "integrity": "sha512-PPjh4gyrQnGe97JTalgRGMuU4icsZFnWkzicB/fUtzlKUqvsWBKEpPPfr5a2JiyirZkHxnAqkQMO5Z5B2kK3fA==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.22.5.tgz", + "integrity": "sha512-/9xnaTTJcVoBtSSmrVyhtSvO3kbqS2ODoh2juEU72c3aYonNF0OMGiaz2gjukyKM2wBBYJP38S4JiE0Wfb5VMQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.22.5.tgz", + "integrity": "sha512-TiOArgddK3mK/x1Qwf5hay2pxI6wCZnvQqrFSqbtg1GLl2JcNMitVH/YnqjP+M31pLUeTfzY1HAXFDnUBV30rQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.22.5.tgz", + "integrity": "sha512-rR7KePOE7gfEtNTh9Qw+iO3Q/e4DEsoQ+hdvM6QUDH7JRJ5qxq5AA52ZzBWbI5i9lfNuvySgOGP8ZN7LAmaiPw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "regenerator-transform": "^0.15.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.22.5.tgz", + "integrity": "sha512-DTtGKFRQUDm8svigJzZHzb/2xatPc6TzNvAIJ5GqOKDsGFYgAskjRulbR/vGsPKq3OPqtexnz327qYpP57RFyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.22.5.tgz", + "integrity": "sha512-bg4Wxd1FWeFx3daHFTWk1pkSWK/AyQuiyAoeZAOkAOUBjnZPH6KT7eMxouV47tQ6hl6ax2zyAWBdWZXbrvXlaw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.3", + "babel-plugin-polyfill-corejs3": "^0.8.1", + "babel-plugin-polyfill-regenerator": "^0.5.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.22.5.tgz", + "integrity": "sha512-vM4fq9IXHscXVKzDv5itkO1X52SmdFBFcMIBZ2FRn2nqVYqw6dBexUgMvAjHW+KXpPPViD/Yo3GrDEBaRC0QYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.22.5.tgz", + "integrity": "sha512-5ZzDQIGyvN4w8+dMmpohL6MBo+l2G7tfC/O2Dg7/hjpgeWvUx8FzfeOKxGog9IimPa4YekaQ9PlDqTLOljkcxg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.22.5.tgz", + "integrity": "sha512-zf7LuNpHG0iEeiyCNwX4j3gDg1jgt1k3ZdXBKbZSoA3BbGQGvMiSvfbZRR3Dr3aeJe3ooWFZxOOG3IRStYp2Bw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.22.5.tgz", + "integrity": "sha512-5ciOehRNf+EyUeewo8NkbQiUs4d6ZxiHo6BcBcnFlgiJfu16q0bQUw9Jvo0b0gBKFG1SMhDSjeKXSYuJLeFSMA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.22.5.tgz", + "integrity": "sha512-bYkI5lMzL4kPii4HHEEChkD0rkc+nvnlR6+o/qdqR6zrm0Sv/nodmyLhlq2DO0YKLUNd2VePmPRjJXSBh9OIdA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.22.5.tgz", + "integrity": "sha512-biEmVg1IYB/raUO5wT1tgfacCef15Fbzhkx493D3urBI++6hpJ+RFG4SrWMn0NEZLfvilqKf3QDrRVZHo08FYg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.22.5.tgz", + "integrity": "sha512-HCCIb+CbJIAE6sXn5CjFQXMwkCClcOfPCzTlilJ8cUatfzwHlWQkbtV0zD338u9dZskwvuOYTuuaMaA8J5EI5A==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.22.5.tgz", + "integrity": "sha512-028laaOKptN5vHJf9/Arr/HiJekMd41hOEZYvNsrsXqJ7YPYuX2bQxh31fkZzGmq3YqHRJzYFFAVYvKfMPKqyg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.22.5.tgz", + "integrity": "sha512-lhMfi4FC15j13eKrh3DnYHjpGj6UKQHtNKTbtc1igvAhRy4+kLhV07OpLcsN0VgDEw/MjAvJO4BdMJsHwMhzCg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.22.5.tgz", + "integrity": "sha512-fj06hw89dpiZzGZtxn+QybifF07nNiZjZ7sazs2aVDcysAZVGjW7+7iFYxg6GLNM47R/thYfLdrXc+2f11Vi9A==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.5", + "@babel/helper-compilation-targets": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.22.5", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.22.5", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.22.5", + "@babel/plugin-syntax-import-attributes": "^7.22.5", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.22.5", + "@babel/plugin-transform-async-generator-functions": "^7.22.5", + "@babel/plugin-transform-async-to-generator": "^7.22.5", + "@babel/plugin-transform-block-scoped-functions": "^7.22.5", + "@babel/plugin-transform-block-scoping": "^7.22.5", + "@babel/plugin-transform-class-properties": "^7.22.5", + "@babel/plugin-transform-class-static-block": "^7.22.5", + "@babel/plugin-transform-classes": "^7.22.5", + "@babel/plugin-transform-computed-properties": "^7.22.5", + "@babel/plugin-transform-destructuring": "^7.22.5", + "@babel/plugin-transform-dotall-regex": "^7.22.5", + "@babel/plugin-transform-duplicate-keys": "^7.22.5", + "@babel/plugin-transform-dynamic-import": "^7.22.5", + "@babel/plugin-transform-exponentiation-operator": "^7.22.5", + "@babel/plugin-transform-export-namespace-from": "^7.22.5", + "@babel/plugin-transform-for-of": "^7.22.5", + "@babel/plugin-transform-function-name": "^7.22.5", + "@babel/plugin-transform-json-strings": "^7.22.5", + "@babel/plugin-transform-literals": "^7.22.5", + "@babel/plugin-transform-logical-assignment-operators": "^7.22.5", + "@babel/plugin-transform-member-expression-literals": "^7.22.5", + "@babel/plugin-transform-modules-amd": "^7.22.5", + "@babel/plugin-transform-modules-commonjs": "^7.22.5", + "@babel/plugin-transform-modules-systemjs": "^7.22.5", + "@babel/plugin-transform-modules-umd": "^7.22.5", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.22.5", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.22.5", + "@babel/plugin-transform-numeric-separator": "^7.22.5", + "@babel/plugin-transform-object-rest-spread": "^7.22.5", + "@babel/plugin-transform-object-super": "^7.22.5", + "@babel/plugin-transform-optional-catch-binding": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.22.5", + "@babel/plugin-transform-parameters": "^7.22.5", + "@babel/plugin-transform-private-methods": "^7.22.5", + "@babel/plugin-transform-private-property-in-object": "^7.22.5", + "@babel/plugin-transform-property-literals": "^7.22.5", + "@babel/plugin-transform-regenerator": "^7.22.5", + "@babel/plugin-transform-reserved-words": "^7.22.5", + "@babel/plugin-transform-shorthand-properties": "^7.22.5", + "@babel/plugin-transform-spread": "^7.22.5", + "@babel/plugin-transform-sticky-regex": "^7.22.5", + "@babel/plugin-transform-template-literals": "^7.22.5", + "@babel/plugin-transform-typeof-symbol": "^7.22.5", + "@babel/plugin-transform-unicode-escapes": "^7.22.5", + "@babel/plugin-transform-unicode-property-regex": "^7.22.5", + "@babel/plugin-transform-unicode-regex": "^7.22.5", + "@babel/plugin-transform-unicode-sets-regex": "^7.22.5", + "@babel/preset-modules": "^0.1.5", + "@babel/types": "^7.22.5", + "babel-plugin-polyfill-corejs2": "^0.4.3", + "babel-plugin-polyfill-corejs3": "^0.8.1", + "babel-plugin-polyfill-regenerator": "^0.5.0", + "core-js-compat": "^3.30.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.5.tgz", + "integrity": "sha512-A57th6YRG7oR3cq/yt/Y84MvGgE0eJG2F1JLhKuyG+jFxEgrd/HAMJatiFtmOiZurz+0DkrvbheCLaV5f2JfjA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", + "@babel/plugin-transform-dotall-regex": "^7.4.4", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, + "node_modules/@babel/runtime": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.22.5.tgz", + "integrity": "sha512-ecjvYlnAaZ/KVneE/OdKYBYfgXV3Ptu6zQWmgEF7vwKhQnvVS6bjMD2XYgj+SNvQ1GfK/pjgokfPkC/2CO8CuA==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.5.tgz", + "integrity": "sha512-X7yV7eiwAxdj9k94NEylvbVHLiVG1nvzCV2EAowhxLTwODV1jl9UzZ48leOC0sH7OnuHrIkllaBgneUykIcZaw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.22.5.tgz", + "integrity": "sha512-7DuIjPgERaNo6r+PZwItpjCZEa5vyw4eJGufeLxrPdBXBoLcCJCIasvK6pK/9DVNrLZTLFhUGqaC6X/PA007TQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.22.5", + "@babel/generator": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.5", + "@babel/helper-function-name": "^7.22.5", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.5", + "@babel/parser": "^7.22.5", + "@babel/types": "^7.22.5", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.22.5.tgz", + "integrity": "sha512-zo3MIHGOkPOfoRXitsgHLjEXmlDaD/5KU1Uzuc9GNiZPhSqVxVRtxuPaSBZDsYZ9qV88AjtMtWW7ww98loJ9KA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.22.5", + "@babel/helper-validator-identifier": "^7.22.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.19.tgz", + "integrity": "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", + "integrity": "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.19.tgz", + "integrity": "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", + "integrity": "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", + "integrity": "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", + "integrity": "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", + "integrity": "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", + "integrity": "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", + "integrity": "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", + "integrity": "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", + "integrity": "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", + "integrity": "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", + "integrity": "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", + "integrity": "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", + "integrity": "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", + "integrity": "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", + "integrity": "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", + "integrity": "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", + "integrity": "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", + "integrity": "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", + "integrity": "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", + "integrity": "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz", + "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", + "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.18", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.18.tgz", + "integrity": "sha512-w+niJYzMHdd7USdiH2U6869nqhD2nbfZXND5Yp93qIbEmnDNk7PD48o+YchRVpzMU7M6jVCbenTR7PA1FLQ9pA==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@jridgewell/trace-mapping/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "dev": true + }, + "node_modules/@material/animation": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/animation/-/animation-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-kqqzG54tabYJ5VsBur5k1bqCFQCEpaW3hmLRMiSVVxRY7XgTt7qkuOOz48gs+MPqR6P8VIi6gFpuscV1+DWDhw==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/auto-init": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/auto-init/-/auto-init-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-8nLe/XeueJg5yyYx5e4UxWQXpTDyUhibKfyroGwnRKc8pdpOCOulHSOj/fIVGJAIbxkEJoebwMadWUNCjUhc9A==", + "dependencies": { + "@material/base": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/banner": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/banner/-/banner-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-gJ4/VdP4dJgHP72Kdjy2f/UjHB45J4CuxoGvI0NIQYUjOSsr4kQiQHsjVgyEPZR/5wa7kBhM7/0mJ+zF7Ghv2A==", + "dependencies": { + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/button": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tokens": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/base": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/base/-/base-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-rW2upYD5YjRFBL6DzYn3SCRhtvpEDkwplDS810e3vt71uLMRyqXyw4OQJH+Nab/t+32TFDtKNUphXIzwICXGDQ==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/button": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/button/-/button-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-SMyqtsvJuCqpXBz2JgciuR6wddNJSGpTXUFxmLbGluBy5/hHm06JWlOFcUOxGDv46OdRGGrRfkg6A9JtvtsJsw==", + "dependencies": { + "@material/density": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/focus-ring": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tokens": "15.0.0-canary.b994146f6.0", + "@material/touch-target": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/card": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/card/-/card-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-WSggGon91HcDhJyatnYLFkoM9glkkeJjyjFDWrcJkwN1rdrPJU+GH+PNjvmArz5hGv9WkmjDjhOdAuPnL4Mb7g==", + "dependencies": { + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tokens": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/checkbox": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/checkbox/-/checkbox-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-pulRiwG9S/dS6WBG+GteODBltddFiL0Sb7HAqdzF2BTKNKv25q1ZIR3ftoEa09TNeWM88AOzTJ4aBHiADfJn2w==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/density": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/focus-ring": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/touch-target": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/chips": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/chips/-/chips-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-3yJPj7x+eKLA4LMKG7aTWI+itAnKRVGOcniuR6aiXVy0OKr5asNuWNeZc9J0/VErjjxF3tdybDzDSPo01qPy9w==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/checkbox": "15.0.0-canary.b994146f6.0", + "@material/density": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/focus-ring": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tokens": "15.0.0-canary.b994146f6.0", + "@material/touch-target": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/circular-progress": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/circular-progress/-/circular-progress-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-6YUvGXdtZKJoE7AuovR4xk1aiWp/EDZ6j2U3TOeynd1assQQCg5XT4abqAoHtpJrRPaCFgUAp836HyiDVVuYug==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/progress-indicator": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/data-table": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/data-table/-/data-table-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-v4hIduIe/wzyibuL/RPM/ErYrt8XpB7fxyQqtV+0JsMpFa8E81QYyvMCS9EJj9m4YdkrQnZgA+vXQlOkhWvmdQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/checkbox": "15.0.0-canary.b994146f6.0", + "@material/density": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/icon-button": "15.0.0-canary.b994146f6.0", + "@material/linear-progress": "15.0.0-canary.b994146f6.0", + "@material/list": "15.0.0-canary.b994146f6.0", + "@material/menu": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/select": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tokens": "15.0.0-canary.b994146f6.0", + "@material/touch-target": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/density": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/density/-/density-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-m8l0vuoWSoAPItBpWp5eZDvitUcB2JWoO8V486hLgdveVcKgXG09xWM43ScH+PLXAWjzr5olDEuJ2tvfkN3SpQ==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dialog": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/dialog/-/dialog-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-JucU92yh8cfZQpyRBunHr6uohacePLYmhcPaGpkAGQ1b+zCznEsNs55tjhaVQNoj91XA9rrBqtL6Otg+fxFJtQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/button": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/icon-button": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tokens": "15.0.0-canary.b994146f6.0", + "@material/touch-target": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/dom": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/dom/-/dom-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-DiUsTezrCi4iytjIn7xXoXZSNFvuTrVVZgc7cR9cW8yu2Hpz8bPf87PacVn4IP9OsNwy/dCDMk1Kcq/DMh7gXQ==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/drawer": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/drawer/-/drawer-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-Kbuf32V0eX69amvCVbAjNSabNDerZWyG8ip466EfQHRh0OUZwvsbhLp9FZOB7AyR+/bQiHf3mVLcombOdmdkcQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/list": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/elevation": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/elevation/-/elevation-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-l2YDNgBajSI6oA2l6gaeYCTGHRao657syqQ/tv95/Hkcee9900A4RrsxCwSxOqqAs5pZZDEJ33kFJjj27nqZDw==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/fab": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/fab/-/fab-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-ExyDVkNWINpns41Ahj4u8I/OhiVkqI0nmcqjFRtgTJMmKEd4NhlvqIxE7gakAlyS68riJu5UleqTSTVmt8mv2Q==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/focus-ring": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tokens": "15.0.0-canary.b994146f6.0", + "@material/touch-target": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/feature-targeting": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/feature-targeting/-/feature-targeting-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-HR/FjSQmza98B1DF80MRjODyfOI9r7wXkPSts/cLQsYkpwZ5uJmxhvQKjDCeYVpMV0lQuvuvVOQo7uD44TdWEg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/floating-label": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/floating-label/-/floating-label-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-g64talBNWCS0FUfLWal0uB637gUciSIqYxFzSW//LglTtbZLGK2J4+9gAEswQGnKeO4ux08EN2n1ZcMDYQ58ow==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/focus-ring": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/focus-ring/-/focus-ring-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-87qEMuXsCvlQfTiimnzJUZoebnIXWcMtRZevNLymN9Y0t9jGckQxZPmrI0llRkpyiR/Ewhec5SI/JGrFlYHnsA==", + "dependencies": { + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0" + } + }, + "node_modules/@material/form-field": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/form-field/-/form-field-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-Tg1SQQaopvXMyDEYxGTWnhCWQmNcWVIoKMLmle9P/gi2p8ulcj0iOCPYf+3ECqUBVozOmTPKlYOOiRwtKStAeA==", + "dependencies": { + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/icon-button": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/icon-button/-/icon-button-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-X6DvOv4jpymHUjI7ZAbO946nDgGYKDwPZfkRzBE84gv2XEr2qfMuABhojxkYubRbt03oauBdcJVVMFCXkVhArQ==", + "dependencies": { + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/density": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/focus-ring": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/touch-target": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/image-list": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/image-list/-/image-list-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-kf903XFF1P+V5ZPXCt+7R6c55g4UyQE1ZHkTViCIJfd52gU40bHODMhTQy/ywBkwDeJfNk8uf1V1IM24WQYpxA==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/layout-grid": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/layout-grid/-/layout-grid-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-OALBSGue8g1/mEwLYYi2d950dJFpNYKW87jPS9/KM65JKMyxoU7tU2d4An1BuyqK0r9sopGq6Pn/zhill0iLaw==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/line-ripple": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/line-ripple/-/line-ripple-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-evjZxCu4iodiKtW8N0xjY8ACRXm3sY+4rAmq3vV5BmHWAJ3BobjbFYslDMZQ+4mu3HmwMatbJehKxHegahitNg==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/linear-progress": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/linear-progress/-/linear-progress-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-jlXh+tIj+/o0Ks7fHdC/24fH6IXCAl2vF52U6NwT39ESrlwmlLhp3gtag5GSBHN5E7Z09nK871Yo1G/b1F+COg==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/progress-indicator": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/list": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/list/-/list-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-kY/i6VvFBb/W3VvCPvWRMzWvu7mvNFJ+R8ijfawDoAXiv4fj42GO4iFyTcFXaUevEPKp791pN/09BMJQ6jYEvA==", + "dependencies": { + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/density": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tokens": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/menu/-/menu-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-y6smNmLJ+U0DoXWbyqzW+VW/uWDuklhdGHc5MbZrTOhsKkhvoTVNMSOa+NFPU4gTwrplvUjaUvnIsQ0wygwD3g==", + "dependencies": { + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/list": "15.0.0-canary.b994146f6.0", + "@material/menu-surface": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tokens": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/menu-surface": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/menu-surface/-/menu-surface-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-StmM3lrRn1iMEZfq532jpMNppqyBBy68FbPurKEsHuP/3q+CscfnwjrS9ym+JcHqXKMHnQXbL/49ymffRGX2AQ==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/notched-outline": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/notched-outline/-/notched-outline-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-UZxU8jXM2t/bk/CiO0K+TSPspuJRZIyrYlIS0gd+qq/u8Gi2DpALBlLAh9Jeu46IUg4YGlPsNWYfe8p3QAVyoA==", + "dependencies": { + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/floating-label": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/progress-indicator": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/progress-indicator/-/progress-indicator-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-VT+mOQhohaM+pBX1rknbVOI6JCGKg9NiOHBoYljIvnexNeILE+mW9g6mtQ0ZCJPz0oMmiSAMLcuxMIcBXx84Xw==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/@material/radio": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/radio/-/radio-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-U/RR2lVNWwEO2+kJtGz9XzvnOF0gAZn1krMY0z/eU9Wnl0OgPZbqQrxXMoVNv1pzKYSEwZQEGado/rv8qp7piA==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/density": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/focus-ring": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/touch-target": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/ripple": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/ripple/-/ripple-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-WzIbc8wYTzMOczqGXVCBPdNcv/73Ef8FwcQYsscGMaqCzgVsdpoqilTfsx7Ryyz6dQbyfmJqp7s+YpPujcezOA==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/rtl": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/rtl/-/rtl-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-H/W6BVn4Ygfkrf/FgSrNhbu1uY7PST2wlsjEYQt06EfAM0CDHEwSL1MwV4FmpQA/r40Q0PqoLN6moDrtCe5S8g==", + "dependencies": { + "@material/theme": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/segmented-button": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/segmented-button/-/segmented-button-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-jd+f4BTnU0tghxBpAM/XdVmruDXSoQ88TYSFWbrhulS+/c/ooCZURWvVC4mHNej+QR/fODkx4adbqkBiwwCtMw==", + "dependencies": { + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/touch-target": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/select": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/select/-/select-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-5thEQS+B17JSm3I8D+mqQe2G3ArVnXJALTEEE9FmMUKwKYkrsLplm3FYuEXERZGJnYeTRdkdmhYY/YeocfZoyA==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/density": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/floating-label": "15.0.0-canary.b994146f6.0", + "@material/line-ripple": "15.0.0-canary.b994146f6.0", + "@material/list": "15.0.0-canary.b994146f6.0", + "@material/menu": "15.0.0-canary.b994146f6.0", + "@material/menu-surface": "15.0.0-canary.b994146f6.0", + "@material/notched-outline": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tokens": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/shape": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/shape/-/shape-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-sINM3gr3aLgdvqZVfqfXV5EB77owLLJjy+2NqchJ8ZPqucCJ+F/BsCBfLA2Wu3O4Sc9IpAEn/o1hzYm/CWAFAw==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/slider": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/slider/-/slider-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-dyT72+Kp//AEajJxDUVoMoizUjf2uggVMGXOaQ7FhpGHuf7LC3EyEjrrJ15efFzYgTjdJUU1YQkCwGmdt6CQsA==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tokens": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/snackbar": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/snackbar/-/snackbar-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-fEhPASJossScNpcrNYrrH8uU+rUf6+kw7/ZMrpUzzz1lVXliL28jTNEmU1nFpcDI4M2GXH+Z64f7vl2hiMDG8g==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/button": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/icon-button": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tokens": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/switch": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/switch/-/switch-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-czCXTUa30ILIf1J3exiuSVIRcodGATHexd3eWDq4sfHo4iMh4rBMaIxcqkmnb2iwE/mMTNyVfoauijx2QiNKrA==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/density": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/focus-ring": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tokens": "15.0.0-canary.b994146f6.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/tab/-/tab-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-ygswooiNdBNNDnQdbPX0nzDQu7oQlHo8vWZ0/xL4IPVEXabY5zCzsEbGNZw2u/syo56c/NHPyMsUmXDGRSXOvQ==", + "dependencies": { + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/focus-ring": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/tab-indicator": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tokens": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-bar": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/tab-bar/-/tab-bar-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-F9NegACnFEWMu1pAAypV4Jd7qROeffkvEgVO28Xxk/CvzZxFz8kAjYJZ+rI6RUhPX3BhXzwsz/AlLwsJMT2tnA==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/density": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/tab": "15.0.0-canary.b994146f6.0", + "@material/tab-indicator": "15.0.0-canary.b994146f6.0", + "@material/tab-scroller": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tokens": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-indicator": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/tab-indicator/-/tab-indicator-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-8IH/DmwlZhQlw/2Y3aKrEvjEhZB+qbKUiyaij3BkTAexvyFeDBh5cLNjRpYkUJSGeSPhS6yu4SYzMHPmQEwQmA==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tab-scroller": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/tab-scroller/-/tab-scroller-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-1MeWkr62OICfTv8oqhIZe6jFo0dKeMlUfB+/WcgnpoeMBszCOSlx5tQ4pedxUkuR3I+Z7rsTfSN0LavgF8bATA==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/tab": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/textfield": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/textfield/-/textfield-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-Kxb3DoJ5o8u3Y1gRMHKmWrDl1TirVxuf/UFrxPFiCE3J1SqiE2VQpakiD1emZwp+LSKtbRsQ/iILYLB/h7Wuvw==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/density": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/floating-label": "15.0.0-canary.b994146f6.0", + "@material/line-ripple": "15.0.0-canary.b994146f6.0", + "@material/notched-outline": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tokens": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/theme": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/theme/-/theme-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-5tsZ92dAeUcZ9g9CrIkqX/GYc0M5DIfsydtI1PAidaBzr1Uokuh4rTZVQZBv7gyglF0yDua59lkb0I6wI9vxXg==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/tokens": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/tokens/-/tokens-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-jFqU7PtvGkrP8b8i2soCrYQInTrnZ1/rIPDi+Xm3sa/qSghCNwFrdJEqwcwtv1fPlJIOtzkIuVRYRmAP9rXQIQ==", + "dependencies": { + "@material/elevation": "15.0.0-canary.b994146f6.0" + } + }, + "node_modules/@material/tooltip": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/tooltip/-/tooltip-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-bVzydXGn3fauHJ8pkh32DsdyRJXleeFQ4t7jZ/rcRik+n4G1BvYiblfuu3Z/OCC0m3TJDyMdJhd+sLqRDqLUUg==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/button": "15.0.0-canary.b994146f6.0", + "@material/dom": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/tokens": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "safevalues": "^0.3.4", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/top-app-bar": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/top-app-bar/-/top-app-bar-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-VHq0wX3OJE1TKvjO8Qtlu+rv5EGoqAhNLBcEjpUUGoqHH/gpd356FEuIqJId4pUh5jaWf8T4ZU9xVbQGMtntzw==", + "dependencies": { + "@material/animation": "15.0.0-canary.b994146f6.0", + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/elevation": "15.0.0-canary.b994146f6.0", + "@material/ripple": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/shape": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "@material/typography": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/touch-target": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/touch-target/-/touch-target-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-X26Y9OWvIqYOHo+sC2VMvOoeQWlUR3/yb7uPdfq92Y44zlQ4Vexgq7nEUblEiXQ8Fj+d0T9rIhRh1y9PP3Z2dw==", + "dependencies": { + "@material/base": "15.0.0-canary.b994146f6.0", + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/rtl": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@material/typography": { + "version": "15.0.0-canary.b994146f6.0", + "resolved": "https://registry.npmjs.org/@material/typography/-/typography-15.0.0-canary.b994146f6.0.tgz", + "integrity": "sha512-sWU5W30WWqdw5P6bsRx9AbvMNcz/QvQg56Syr06V6nfgSztpeuo7TfPk2J+N0ArRALo1mUrkAPk66iWYQ2p/QA==", + "dependencies": { + "@material/feature-targeting": "15.0.0-canary.b994146f6.0", + "@material/theme": "15.0.0-canary.b994146f6.0", + "tslib": "^2.1.0" + } + }, + "node_modules/@ngtools/webpack": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-16.1.3.tgz", + "integrity": "sha512-YTL1RzP7ErJqskx+ZwdC/nWsOSBfC4yYWmMyWL2J0d+oJ3N2XIzrKVoDcZ4IVzv3Du+3zoGp0ups/wWXvfzM/Q==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^16.0.0", + "typescript": ">=4.9.3 <5.2", + "webpack": "^5.54.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/fs": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.0.tgz", + "integrity": "sha512-7kZUAaLscfgbwBQRbvdMYaZOWyMEcPTH/tJjnyAWJ/dvvs9Ef+CERx/qJb9GExJpl1qipaDGn7KqHnFGGixd0w==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-4.1.0.tgz", + "integrity": "sha512-9hwoB3gStVfa0N31ymBmrX+GuDGdVA/QWShZVqE0HK2Af+7QGGrCTbZia/SW0ImUTjTne7SP91qxDmtXvDHRPQ==", + "dev": true, + "dependencies": { + "@npmcli/promise-spawn": "^6.0.0", + "lru-cache": "^7.4.4", + "npm-pick-manifest": "^8.0.0", + "proc-log": "^3.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-2.0.2.tgz", + "integrity": "sha512-xACzLPhnfD51GKvTOOuNX2/V4G4mz9/1I2MfDoye9kBM3RYe5g2YbscsaGoTlaWqkxeiapBWyseULVKpSVHtKQ==", + "dev": true, + "dependencies": { + "npm-bundled": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "bin": { + "installed-package-contents": "lib/index.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-3.0.0.tgz", + "integrity": "sha512-gp8pRXC2oOxu0DUE1/M3bYtb1b3/DbJ5aM113+XJBgfXdussRAsX0YOrOhdd8WvnAR6auDBvJomGAkLKA5ydxA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-6.0.2.tgz", + "integrity": "sha512-gGq0NJkIGSwdbUt4yhdF8ZrmkGKVz9vAdVzpOfnom+V8PLSmSOVhZwbNvZZS1EYcJN5hzzKBxmmVVAInM6HQLg==", + "dev": true, + "dependencies": { + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-6.0.2.tgz", + "integrity": "sha512-NCcr1uQo1k5U+SYlnIrbAh3cxy+OQT1VtqiAbxdymSlptbzBb62AjH2xXgjNCoP073hoa1CfCAcwoZ8k96C4nA==", + "dev": true, + "dependencies": { + "@npmcli/node-gyp": "^3.0.0", + "@npmcli/promise-spawn": "^6.0.0", + "node-gyp": "^9.0.0", + "read-package-json-fast": "^3.0.0", + "which": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@schematics/angular": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.1.3.tgz", + "integrity": "sha512-bNSxCLf6f+/dsQ1k3PhcZhrC/qgJSCpM6h3m6ATpjR+tYW/v7WR1OyE5r3DQmDe7NJSazBvpbrRtg8xjRsMzvw==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "16.1.3", + "@angular-devkit/schematics": "16.1.3", + "jsonc-parser": "3.2.0" + }, + "engines": { + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.1.0.tgz", + "integrity": "sha512-a31EnjuIDSX8IXBUib3cYLDRlPMU36AWX4xS8ysLaNu4ZzUesDiPt83pgrW2X1YLMe5L2HbDyaKK5BrL4cNKaQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-1.0.0.tgz", + "integrity": "sha512-bLzi9GeZgMCvjJeLUIfs8LJYCxrPRA8IXQkzUtaFKKVPTz0mucRyqFcV2U20yg9K+kYAD0YSitzGfRZCFLjdHQ==", + "dev": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.1.0", + "make-fetch-happen": "^11.0.1", + "tuf-js": "^1.1.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==", + "dev": true + }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@tufjs/canonical-json": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-1.0.0.tgz", + "integrity": "sha512-QTnf++uxunWvG2z3UFNzAoQPHxnSXOwtaI3iJ+AohhV+5vONuArPjJE7aPXPVXfXJsqrVbZBu9b81AJoSd09IQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-1.0.4.tgz", + "integrity": "sha512-qaGV9ltJP0EO25YfFUPhxRVK0evXFIAGicsVXuRim4Ed9cjPxYhNnNJ49SFmbeLgtxpslIkX317IgpfcHPVj/A==", + "dev": true, + "dependencies": { + "@tufjs/canonical-json": "1.0.0", + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.2", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.2.tgz", + "integrity": "sha512-ALYone6pm6QmwZoAgeyNksccT9Q4AWZQ6PvfwR37GT6r6FWUPguq6sUmNGSMV2Wr761oQoBxwGGa6DR5o1DC9g==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.10", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.10.tgz", + "integrity": "sha512-p7ienRMiS41Nu2/igbJxxLDWrSZ0WxM8UQgCeO9KhoVF7cOVFkrKsiDr1EsJIla8vV3oEEjGcz11jc5yimhzZw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect": { + "version": "3.4.35", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", + "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.0.tgz", + "integrity": "sha512-4x5FkPpLipqwthjPsF7ZRbOv3uoLUFkTA9G9v583qi4pACvq0uTELrB8OLUzPWUI4IJIyvM85vzkV1nyiI2Lig==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "dev": true + }, + "node_modules/@types/cors": { + "version": "2.8.13", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.13.tgz", + "integrity": "sha512-RG8AStHlUiV5ysZQKq97copd2UmVYw3/pRMLefISZ3S1hK104Cwm7iLQ3fTKx+lsUH2CE8FlLaYeEA2LSeqYUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "8.40.2", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.40.2.tgz", + "integrity": "sha512-PRVjQ4Eh9z9pmmtaq8nTjZjQwKFk7YIHIud3lRoKRBgUQjgjRmoGxxGEPXQkF+lH7QkHJRNr5F4aBgYCW0lqpQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.17", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.17.tgz", + "integrity": "sha512-Q4FmmuLGBG58btUnfS1c1r/NQdlp3DMfGDGig8WhfpA2YRUtEkxAjkZb0yvplJGYdF1fsQ81iMDcH24sSCNC/Q==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "4.17.35", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.35.tgz", + "integrity": "sha512-wALWQwrgiB2AWTT91CB62b6Yt0sNHpznUXeZEcnPU3DRdlDIz74x8Qg1UUYKSVFi+va5vKOLYRBI1bRKiLLKIg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-/K3ds8TRAfBvi5vfjuz8y6+GiAYBZ0x4tXv1Av6CWBWn0IlADc+ZX9pMq7oU0fNQPnBwIZl3rmeLp6SBApbxSQ==", + "dev": true + }, + "node_modules/@types/http-proxy": { + "version": "1.17.11", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.11.tgz", + "integrity": "sha512-HC8G7c1WmaF2ekqpnFq626xd3Zz0uvaqFmBJNRZCGEZCXkvSdJoNFn/8Ygbd9fKNQj8UzLdCETaI0UWPAjK7IA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "4.3.5", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-4.3.5.tgz", + "integrity": "sha512-9YHUdvuNDDRJYXZwHqSsO72Ok0vmqoJbNn73ttyITQp/VA60SarnZ+MPLD37rJAhVoKp+9BWOvJP5tHIRfZylQ==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "dev": true + }, + "node_modules/@types/mime": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", + "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==", + "dev": true + }, + "node_modules/@types/node": { + "version": "20.3.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.3.2.tgz", + "integrity": "sha512-vOBLVQeCQfIcF/2Y7eKFTqrMnizK5lRNQ7ykML/5RuwVXVWxYkgwS7xbt4B6fKCUPgbSL5FSsjHQpaGQP/dQmw==", + "dev": true + }, + "node_modules/@types/qs": { + "version": "6.9.7", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", + "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", + "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", + "dev": true + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", + "integrity": "sha512-Cwo8LE/0rnvX7kIIa3QHCkcuF21c05Ayb0ZfxPiv0W8VRiZiNW/WuRupHKpqqGVGf7SUA44QSOUKaEd9lIrd/Q==", + "dev": true, + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-d/Hs3nWDxNL2xAczmOVZNj92YZCS6RGxfBPjKzuu/XirCgXdpKEb88dYNbrYGint6IVWLNP+yonwVAuRC0T2Dg==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.2.tgz", + "integrity": "sha512-J2LqtvFYCzaj8pVYKw8klQXrLLk7TBZmQ4ShlcdkELFKGwGMfevMLneMMRkMgZxotOD9wg497LpC7O8PcvAmfw==", + "dev": true, + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.33", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.33.tgz", + "integrity": "sha512-f0KEEe05NvUnat+boPTZ0dgaLZ4SfSouXUgv5noUiefG2ajgKjmETo9ZJyuqsl7dfl2aHlLJUiki6B4ZYldiiw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.5", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.5.tgz", + "integrity": "sha512-lwhs8hktwxSjf9UaZ9tG5M03PGogvFaH8gUgLNbN9HKIg0dvv6q+gkSuJ8HN4/VbyxkuLzCjlN7GquQ0gUJfIg==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.0.1.tgz", + "integrity": "sha512-pcub+YbFtFhaGRTo1832FQHQSHvMrlb43974e2eS8EKleR3p1cDdkJFPci1UhwkEf1J9Bz+wKBSzqpKp7nNj2A==", + "dev": true, + "engines": { + "node": ">=14.6.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.6", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", + "dev": true + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "dev": true + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.9.0.tgz", + "integrity": "sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.3.0.tgz", + "integrity": "sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "depd": "^2.0.0", + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.12.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz", + "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "dev": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "dev": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/array-flatten": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", + "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.14", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.14.tgz", + "integrity": "sha512-FQzyfOsTlwVzjHxKEqRIAdJx9niO6VCBCoEwax/VLSoQF29ggECcPuBqUMZ+u8jCZOPSy8b8/8KnuFbp0SaFZQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.21.5", + "caniuse-lite": "^1.0.30001464", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-loader": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.1.2.tgz", + "integrity": "sha512-mN14niXW43tddohGl8HPu5yfQq70iUThvFL/4QzESA7GcZoC0eVOhvWdQ8+3UlSjaDE9MVtsW9mxDY07W7VpVA==", + "dev": true, + "dependencies": { + "find-cache-dir": "^3.3.2", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.3.tgz", + "integrity": "sha512-bM3gHc337Dta490gg+/AseNB9L4YLHxq1nGKZZSHbhXv4aTYU2MD2cjza1Ru4S6975YLTaL1K8uJf6ukJhhmtw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.17.7", + "@babel/helper-define-polyfill-provider": "^0.4.0", + "semver": "^6.1.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.8.1.tgz", + "integrity": "sha512-ikFrZITKg1xH6pLND8zT14UPgjKHiGLqex7rGEZCH2EvhsneJaJPemmpQaIZV5AL03II+lXylw3UmddDK8RU5Q==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.0", + "core-js-compat": "^3.30.1" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.5.0.tgz", + "integrity": "sha512-hDJtKjMLVa7Z+LwnTCxoDLQj6wdc+B8dun7ayF2fYieI6OzfuvcLMB32ihJZ4UhCBwNYGl5bg/x/P9cMdnkc2g==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.4.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/bonjour-service": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.1.1.tgz", + "integrity": "sha512-Z/5lQRMOG9k7W+FkeGTNjh7htqn/2LMnfOvBZ8pynNZCM9MwkQkI3zeI4oz09uWdcgmgHugVvBqxGg4VQJ5PCg==", + "dev": true, + "dependencies": { + "array-flatten": "^2.1.2", + "dns-equal": "^1.0.0", + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.9", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.9.tgz", + "integrity": "sha512-M0MFoZzbUrRU4KNfCrDLnvyE7gub+peetoTid3TBIqtunaDJyXlwhakT+/VkvSXcfIzFfK/nkCs4nmyTmxdNSg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001503", + "electron-to-chromium": "^1.4.431", + "node-releases": "^2.0.12", + "update-browserslist-db": "^1.0.11" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", + "dev": true, + "dependencies": { + "semver": "^7.0.0" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "17.1.3", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-17.1.3.tgz", + "integrity": "sha512-jAdjGxmPxZh0IipMdR7fK/4sDSrHMLUV0+GvVUsjwyGNKHsh79kW/otg+GkbXwl6Uzvy9wsvHOX4nUoWldeZMg==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/call-bind": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", + "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "get-intrinsic": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001509", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001509.tgz", + "integrity": "sha512-2uDDk+TRiTX5hMcUYT/7CSyzMZxjfGu0vAUjS2g0LSD8UoXOv0LtpH4LxGMemsiPq6LCVIUjNwVM0erkOkGCDA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz", + "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "dev": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.4", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", + "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.5", + "bytes": "3.0.0", + "compressible": "~2.0.16", + "debug": "2.6.9", + "on-headers": "~1.0.2", + "safe-buffer": "5.1.2", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/bytes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", + "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/compression/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", + "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", + "dev": true, + "dependencies": { + "fast-glob": "^3.2.11", + "glob-parent": "^6.0.1", + "globby": "^13.1.1", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.31.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.31.0.tgz", + "integrity": "sha512-hM7YCu1cU6Opx7MXNu0NuumM0ezNeAeRKadixyiQELWY3vT3De9S4J5ZBMraWV2vZnrE1Cirl0GtFtDtMUXzPw==", + "dev": true, + "dependencies": { + "browserslist": "^4.21.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.2.0.tgz", + "integrity": "sha512-3rTMnFJA1tCOPwRxtgF4wd7Ab2qvDbL8jX+3smjIbS4HlZBagTlpERbdN7iAbWlrfxE3M8c27kTwTawQ7st+OQ==", + "dev": true, + "dependencies": { + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "parse-json": "^5.0.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + } + }, + "node_modules/cosmiconfig/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/cosmiconfig/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/critters": { + "version": "0.0.19", + "resolved": "https://registry.npmjs.org/critters/-/critters-0.0.19.tgz", + "integrity": "sha512-Fm4ZAXsG0VzWy1U30rP4qxbaWGSsqXDgSupJW1OUJGDAs0KWC+j37v7p5a2kZ9BPJvhRzWm3be+Hc9WvQOBUOw==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "css-select": "^5.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.2", + "htmlparser2": "^8.0.2", + "postcss": "^8.4.23", + "pretty-bytes": "^5.3.0" + } + }, + "node_modules/critters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/critters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/critters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/critters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/critters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/critters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.8.1.tgz", + "integrity": "sha512-xDAXtEVGlD0gJ07iclwWVkLoZOpEvAWaSyf6W18S2pOC//K8+qUDIx8IIT3D+HjnmkJPQeesOPv5aiUaJsCM2g==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.21", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.3", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-equal": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", + "integrity": "sha512-z+paD6YUQsk+AbGCEM4PrOXSss5gd66QfcVBFTKR/HpFL9jCqikS94HYwKww6fQyO7IxrIIyUu+g0Ka9tUS2Cg==", + "dev": true + }, + "node_modules/dns-packet": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.0.tgz", + "integrity": "sha512-rza3UH1LwdHh9qyPXp8lkwpjSNk/AMD3dPytUoRoqnypDUhY0xvbdmVhWOfxO68frEfV9BU8V12Ez7ZsHGZpCQ==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.445", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.445.tgz", + "integrity": "sha512-++DB+9VK8SBJwC+X1zlMfJ1tMA3F0ipi39GdEp+x3cV2TyBihqAgad8cNMWtLDEkbH39nlDQP7PfGrDr3Dr7HA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.5.1", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.1.tgz", + "integrity": "sha512-mGqhI+D7YxS9KJMppR6Iuo37Ed3abhU8NdfgSvJSDUafQutrN+sPTncJYTyM9+tkhSmWodKtVYGPPHyXJEwEQA==", + "dev": true, + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.1.0", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.1.0.tgz", + "integrity": "sha512-enySgNiK5tyZFynt3z7iqBR+Bto9EVVVvDFuTT0ioHCGbzirZVGDGiQjZzEp8hWl6hd5FSVytJGuScX1C1C35w==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", + "integrity": "sha512-GHrMyVZQWvTIdDtpiEXdHZnFQKzeO09apj8Cbl4pKWy4i0Oprcq17usfDt5aO63swf0JOeMWjWQE/LzgSRuWpA==", + "dev": true + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "devOptional": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", + "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.17.19.tgz", + "integrity": "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.17.19", + "@esbuild/android-arm64": "0.17.19", + "@esbuild/android-x64": "0.17.19", + "@esbuild/darwin-arm64": "0.17.19", + "@esbuild/darwin-x64": "0.17.19", + "@esbuild/freebsd-arm64": "0.17.19", + "@esbuild/freebsd-x64": "0.17.19", + "@esbuild/linux-arm": "0.17.19", + "@esbuild/linux-arm64": "0.17.19", + "@esbuild/linux-ia32": "0.17.19", + "@esbuild/linux-loong64": "0.17.19", + "@esbuild/linux-mips64el": "0.17.19", + "@esbuild/linux-ppc64": "0.17.19", + "@esbuild/linux-riscv64": "0.17.19", + "@esbuild/linux-s390x": "0.17.19", + "@esbuild/linux-x64": "0.17.19", + "@esbuild/netbsd-x64": "0.17.19", + "@esbuild/openbsd-x64": "0.17.19", + "@esbuild/sunos-x64": "0.17.19", + "@esbuild/win32-arm64": "0.17.19", + "@esbuild/win32-ia32": "0.17.19", + "@esbuild/win32-x64": "0.17.19" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.17.19", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.17.19.tgz", + "integrity": "sha512-X9UQEMJMZXwlGCfqcBmJ1jEa+KrLfd+gCBypO/TSzo5hZvbVwFqpxj1YCuX54ptTF75wxmrgorR4RL40AKtLVg==", + "dev": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter-asyncresource": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/eventemitter-asyncresource/-/eventemitter-asyncresource-1.0.0.tgz", + "integrity": "sha512-39F7TBIV0G7gTelxwbEqnwhp90eqCPON1k0NwNfwhgKn4Co4ybUbj2pECcXT0B3ztRKZ7Pw1JujUUgmQJHcVAQ==", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/express/node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/express/node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/express/node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.2", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.2.tgz", + "integrity": "sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.1.1.tgz", + "integrity": "sha512-TMKDUnIte6bfb5nWv7V/caI169OHgvwjb7V4WkeUvbQQdjr5rWKqHFiKWb/fcOwB+CzBT+qbWjvj+DVwRskpIg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.2.tgz", + "integrity": "sha512-2GAfyfoaCDRrM6jaOS3UsBts8yJ55VioXdWcOL7dK9zdAuKT71+WBA4ifnNYqVjYv+4SsPxjK0JT4yIIn4cA/g==", + "dev": true, + "dependencies": { + "minipass": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.4.tgz", + "integrity": "sha512-INM/fWAxMICjttnD0DX1rBvinKskj5G1w+oy/pnm9u/tSlnBrzFonJMcalKJ30P8RRsPzKcCG7Q8l0jx5Fh9YQ==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "dev": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz", + "integrity": "sha512-2DcsyfABl+gVHEfCOaTrWgyt+tb6MSEGmKq+kI5HwLbIYgjgmMcV8KQ41uaKz1xxUcn9tJtgFbQUEVcEbd0FYw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.1.tgz", + "integrity": "sha512-9BKYcEeIs7QwlCYs+Y3GBvqAMISufUS0i2ELd11zpZjxI5V9iyRj0HgzB5/cLf2NY4vcYBTYzJ7GIui7j/4DOw==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^2.0.3", + "minimatch": "^9.0.1", + "minipass": "^5.0.0 || ^6.0.2", + "path-scurry": "^1.10.0" + }, + "bin": { + "glob": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.0.tgz", + "integrity": "sha512-jWsQfayf13NvqKUIL3Ta+CIqMnvlaIDFveWE/dpOZ9+3AMEJozsxDvKA02zync9UuvOM8rOXzsD5GqKP4OnWPQ==", + "dev": true, + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.11", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "dev": true + }, + "node_modules/hdr-histogram-js": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hdr-histogram-js/-/hdr-histogram-js-2.0.3.tgz", + "integrity": "sha512-Hkn78wwzWHNCp2uarhzQ2SGFLU3JY8SBDDd3TAABK4fc30wm+MuPOrg5QVFVfkKOQd6Bfz3ukJEI+q9sXEkK1g==", + "dev": true, + "dependencies": { + "@assemblyscript/loader": "^0.10.1", + "base64-js": "^1.2.0", + "pako": "^1.0.3" + } + }, + "node_modules/hdr-histogram-percentiles-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hdr-histogram-percentiles-obj/-/hdr-histogram-percentiles-obj-3.0.0.tgz", + "integrity": "sha512-7kIufnBqdsBGcSZLPJwqHT3yhk1QTsSlFsVD3kx5ixH/AlgBs9yM1q6DPhXZ8f8gtdqgh7N7/5btRLpQsS2gHw==", + "dev": true + }, + "node_modules/hosted-git-info": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-6.1.1.tgz", + "integrity": "sha512-r0EI+HBMcXadMrugk0GCQ+6BQV39PiWAZVfq7oIckeGiN7sjRGyQxPdft3nQekFTCQbYxLBH+/axZMeH8UX6+w==", + "dev": true, + "dependencies": { + "lru-cache": "^7.5.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.4.0.tgz", + "integrity": "sha512-igBTJcNNNhvZFRtm8uA6xMY6xYleeDwn3PeBCkDz7tHttv4F2hsDI2aPgNERWzvRcNYHNT3ymRaQzllmXj4YsQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "dev": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz", + "integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dev": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-6.0.3.tgz", + "integrity": "sha512-C7FfFoTA+bI10qfeydT8aZbvr91vAEU+2W5BZUlzPec47oNb07SsOfwYrtxuvOYdUApPP/Qlh4DtAO51Ekk2QA==", + "dev": true, + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.0.tgz", + "integrity": "sha512-0AOCmOip+xgJwEVTQj1EfiDDOkPmuyllDuTuEX+DDXUgapLAsBIfkg3sxCYyCEA8mQqZrrxPUGjcOQ2JS3WLkg==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/ip": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ip/-/ip-2.0.0.tgz", + "integrity": "sha512-WKa+XuLG1A1R0UWhl2+1XQSi+fZWMsYKffMZTTYsiZaUD8k2yDAj5atimTUD2TZkyCkNEeYE5NhFZmupOGtjYQ==", + "dev": true + }, + "node_modules/ipaddr.js": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz", + "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.12.1.tgz", + "integrity": "sha512-Q4ZuBAe2FUsKtyQJoQHlvP8OvBERxO3jEmy1I7hcRXcJBGGHFh/aJBswbXuS9sgrDH2QUO8ilkwNPHvHMd8clg==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^3.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz", + "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-2.2.1.tgz", + "integrity": "sha512-MXbxovZ/Pm42f6cDIDkl3xpwv1AGwObKwfmjs2nQePiy85tP3fatofl3FC1aBsOtP/6fq5SbtgHwWcMsLP+bDw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jasmine-core": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.0.tgz", + "integrity": "sha512-O236+gd0ZXS8YAjFx8xKaJ94/erqUliEkJTDedyE7iHvv4ZVqi+q+8acJxu05/WJDKm512EUNn809In37nWlAQ==", + "dev": true + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.18.2", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.18.2.tgz", + "integrity": "sha512-QAdOptna2NYiSSpv0O/BwoHBSmz4YhpzJHyi+fnMRTXFjp7B8i/YG5Z8IfusxB1ufjcD2Sre1F3R+nX3fvy7gg==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/karma": { + "version": "6.4.2", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.2.tgz", + "integrity": "sha512-C6SU/53LB31BEgRg+omznBEMY4SjHU3ricV6zBcAe1EeILKkeScr+fZXtaI5WyDbkVowJxxAI6h73NcFPmXolQ==", + "dev": true, + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.4.1", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.2.0.tgz", + "integrity": "sha512-rE9RkUPI7I9mAxByQWkGJFXfFD6lE4gC5nPuZdobf/QdTEJI6EU4yIay/cfU/xV4ZxlM5JiTv7zWYgA64NpS5Q==", + "dev": true, + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-chrome-launcher/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-coverage/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma-coverage/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz", + "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==", + "dev": true, + "peerDependencies": { + "jasmine-core": "^4.0.0 || ^5.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/karma/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", + "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", + "dev": true, + "dependencies": { + "rimraf": "^3.0.0" + }, + "engines": { + "node": ">=8.17.0" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/launch-editor": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.0.tgz", + "integrity": "sha512-JpDCcQnyAAzZZaZ7vEiSqL690w7dAEyLao+KC96zBplnYbJS7TYNjvM3M7y3dGz+v7aIsJk3hllWuc0kWAjyRQ==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.7.3" + } + }, + "node_modules/less": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/less/-/less-4.1.3.tgz", + "integrity": "sha512-w16Xk/Ta9Hhyei0Gpz9m7VS8F28nieJaL/VyShID7cYvP6IL5oHeL6p4TXSDJqZE/lNv0oJ2pGVjJsRkfwm5FA==", + "dev": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-11.1.0.tgz", + "integrity": "sha512-C+uDBV7kS7W5fJlUjq5mPBeBVhYpTIm5gB09APT9o3n/ILeaXVsiSFTbZpTJCJwQ/Crczfn3DmfQFwxYusWFug==", + "dev": true, + "dependencies": { + "klona": "^2.0.4" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.2.1.tgz", + "integrity": "sha512-ZvFw1KWS3GVyYBYb7qkmRM/WwL2TQQBxgCK62rlvm4WpVQ23Nb4tYjApUlfjrEGvOs7KHEsmyUn75OHZrJMWPw==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-fetch-happen": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-11.1.1.tgz", + "integrity": "sha512-rLWS7GCSTcEujjVBs2YqG7Y4643u8ucvCJeSRqiLYhesrDuzeuFIk37xREzAsfQaqzl8b9rNCE4m6J8tvX4Q8w==", + "dev": true, + "dependencies": { + "agentkeepalive": "^4.2.1", + "cacache": "^17.0.0", + "http-cache-semantics": "^4.1.1", + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^7.7.1", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^7.0.0", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/make-fetch-happen/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==", + "dev": true + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.7.6", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.7.6.tgz", + "integrity": "sha512-Qk7HcgaPkGG6eD77mLvZS1nmxlao3j+9PkrT9Uc7HAE1id3F41+DdBRYRYkbyfNRGzm8/YWtzhw7nVPmwhqTQw==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.2.tgz", + "integrity": "sha512-PZOT9g5v2ojiTL7r1xF6plNHLtOeTpSlDI007As2NlA2aYBMfVom17yqa6QzhmDP8QOhn7LjHTg7DFCVSSa6yg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-collect/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-fetch": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.3.tgz", + "integrity": "sha512-n5ITsTkDqYkYJZjcRWzZt9qnZKCT7nKCosJhHoj7S7zD+BP4jVbWs+odsniw5TA3E0sLomhTKOKjF86wf11PuQ==", + "dev": true, + "dependencies": { + "minipass": "^5.0.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-json-stream": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz", + "integrity": "sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg==", + "dev": true, + "dependencies": { + "jsonparse": "^1.3.1", + "minipass": "^3.0.0" + } + }, + "node_modules/minipass-json-stream/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-json-stream/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/nanoid": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/needle": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz", + "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==", + "dev": true, + "optional": true, + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "optional": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/nice-napi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz", + "integrity": "sha512-px/KnJAJZf5RuBGcfD+Sp2pAKq0ytz8j+1NehvgIGFkvtvFrDM3T8E4x/JJODXK9WZow8RRGrbA9QQ3hs+pDhA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "!win32" + ], + "dependencies": { + "node-addon-api": "^3.0.0", + "node-gyp-build": "^4.2.2" + } + }, + "node_modules/node-addon-api": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-3.2.1.tgz", + "integrity": "sha512-mmcei9JghVNDYydghQmeDX8KoAm0FAiYyIcUt/N4nhyAipB17pllZQDOJD2fotxABnt4Mdz+dKTO7eftLg4d0A==", + "dev": true, + "optional": true + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-9.4.0.tgz", + "integrity": "sha512-dMXsYP6gc9rRbejLXmTbVRYjAHw7ppswsKyMxuxJxxOHzluIO1rGp9TOQgjFJ+2MCqcOcQTOPB/8Xwhr+7s4Eg==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^11.0.3", + "nopt": "^6.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^12.13 || ^14.13 || >=16" + } + }, + "node_modules/node-gyp-build": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.6.0.tgz", + "integrity": "sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ==", + "dev": true, + "optional": true, + "bin": { + "node-gyp-build": "bin.js", + "node-gyp-build-optional": "optional.js", + "node-gyp-build-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/node-releases": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.12.tgz", + "integrity": "sha512-QzsYKWhXTWx8h1kIvqfnC++o0pEmpRQA/aenALsL2F4pqNVr7YzcdMlDij5WBnwftRbJCNJL/O7zdKaxKPHqgQ==", + "dev": true + }, + "node_modules/nopt": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-6.0.0.tgz", + "integrity": "sha512-ZwLpbTgdhuZUnZzjd7nb1ZV+4DoiC6/sfiVKok72ym/4Tlf+DFdlHYmT2JPmcNNWV6Pi3SDf1kT+A4r9RTuT9g==", + "dev": true, + "dependencies": { + "abbrev": "^1.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/normalize-package-data": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-5.0.0.tgz", + "integrity": "sha512-h9iPVIfrVZ9wVYQnxFgtw1ugSvGEMOlyPWWtm8BMJhnwyEL/FLbYbTY3V3PpjI/BUK67n9PEWDu6eHzu1fB15Q==", + "dev": true, + "dependencies": { + "hosted-git-info": "^6.0.0", + "is-core-module": "^2.8.1", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-3.0.0.tgz", + "integrity": "sha512-Vq0eyEQy+elFpzsKjMss9kxqb9tG3YHg4dsyWuUENuzvSUWe1TCnW/vV9FkhvBk/brEDoDiVd+M1Btosa6ImdQ==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-install-checks": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-6.1.1.tgz", + "integrity": "sha512-dH3GmQL4vsPtld59cOn8uY0iOqRmqKvV+DLGwNXV/Q7MDgD2QfOADWd/mFXcIE5LVhYYGjA3baz6W9JneqnuCw==", + "dev": true, + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-3.0.1.tgz", + "integrity": "sha512-dMxCf+zZ+3zeQZXKxmyuCKlIDPGuv8EF940xbkC4kQVDTtqoh6rJFO+JTKSA6/Rwi0getWmtuy4Itup0AMcaDQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-package-arg": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-10.1.0.tgz", + "integrity": "sha512-uFyyCEmgBfZTtrKk/5xDfHp6+MdrqGotX/VoOyEEl3mBwiEE5FlBaePanazJSVMPT7vKepcjYBY2ztg9A3yPIA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^6.0.0", + "proc-log": "^3.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-packlist": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-7.0.4.tgz", + "integrity": "sha512-d6RGEuRrNS5/N84iglPivjaJPxhDbZmlbTwTDX2IbcRHG5bZCdtysYMhwiPvcF4GisXHGn7xsxv+GQ7T/02M5Q==", + "dev": true, + "dependencies": { + "ignore-walk": "^6.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-8.0.1.tgz", + "integrity": "sha512-mRtvlBjTsJvfCCdmPtiu2bdlx8d/KXtF7yNXNWe7G0Z36qWA9Ny5zXsI2PfBZEv7SXgoxTmNaTzGSbbzDZChoA==", + "dev": true, + "dependencies": { + "npm-install-checks": "^6.0.0", + "npm-normalize-package-bin": "^3.0.0", + "npm-package-arg": "^10.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "14.0.5", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-14.0.5.tgz", + "integrity": "sha512-kIDMIo4aBm6xg7jOttupWZamsZRkAqMqwqqbVXnUqstY5+tapvv6bkH/qMR76jdgV+YljEUCyWx3hRYMrJiAgA==", + "dev": true, + "dependencies": { + "make-fetch-happen": "^11.0.0", + "minipass": "^5.0.0", + "minipass-fetch": "^3.0.0", + "minipass-json-stream": "^1.0.1", + "minizlib": "^2.1.2", + "npm-package-arg": "^10.0.0", + "proc-log": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "dev": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.12.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz", + "integrity": "sha512-geUvdk7c+eizMNUDkRpW1wJwgfOiOeHbxBR/hLXK1aT6zmVSO0jsQcs7fj6MGw89jC/cjGfLcNOrtMYtGqm81g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/ora/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/ora/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pacote": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.2.0.tgz", + "integrity": "sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA==", + "dev": true, + "dependencies": { + "@npmcli/git": "^4.0.0", + "@npmcli/installed-package-contents": "^2.0.1", + "@npmcli/promise-spawn": "^6.0.1", + "@npmcli/run-script": "^6.0.0", + "cacache": "^17.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^5.0.0", + "npm-package-arg": "^10.0.0", + "npm-packlist": "^7.0.0", + "npm-pick-manifest": "^8.0.0", + "npm-registry-fetch": "^14.0.0", + "proc-log": "^3.0.0", + "promise-retry": "^2.0.1", + "read-package-json": "^6.0.0", + "read-package-json-fast": "^3.0.0", + "sigstore": "^1.3.0", + "ssri": "^10.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "lib/bin.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", + "dev": true + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "devOptional": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.0.tgz", + "integrity": "sha512-tZFEaRQbMLjwrsmidsGJ6wDMv0iazJWk6SfIKnY4Xru8auXgmJkOBa5DUbYFcFD2Rzk2+KDlIiF0GVXNCbgC7g==", + "dev": true, + "dependencies": { + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.0.tgz", + "integrity": "sha512-svTf/fzsKHffP42sujkO/Rjs37BCIsQVRCeNYIm9WN8rgT7ffoUnRtZCqU+6BqcSBdv8gwJeTz8knJpgACeQMw==", + "dev": true, + "engines": { + "node": "14 || >=16.14" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/piscina": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-3.2.0.tgz", + "integrity": "sha512-yn/jMdHRw+q2ZJhFhyqsmANcbF6V2QwmD84c6xRau+QpQOmtrBCoRGdvTfeuFDYXB5W2m6MfLkjkvQa9lUSmIA==", + "dev": true, + "dependencies": { + "eventemitter-asyncresource": "^1.0.0", + "hdr-histogram-js": "^2.0.1", + "hdr-histogram-percentiles-obj": "^3.0.0" + }, + "optionalDependencies": { + "nice-napi": "^1.0.2" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.4.24", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.24.tgz", + "integrity": "sha512-M0RzbcI0sO/XJNucsGjvWU9ERWxb/ytp1w6dKtxTKgixdtQDq4rmx/g8W1hnaheq9jgwL/oyEdH5Bc4WwJKMqg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-loader": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.2.tgz", + "integrity": "sha512-c7qDlXErX6n0VT+LUsW+nwefVtTu3ORtVvK8EXuUIDcxo+b/euYqpuHlJAvePb0Af5e8uMjR/13e0lTuYifaig==", + "dev": true, + "dependencies": { + "cosmiconfig": "^8.1.3", + "jiti": "^1.18.2", + "klona": "^2.0.6", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.3.tgz", + "integrity": "sha512-2/u2zraspoACtrbFRnTijMiQtb4GW4BvatjaG/bCjYQo8kLTdevCUlwuBHx2sCnSyrI3x3qj4ZK1j5LQBgzmwA==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.13", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.13.tgz", + "integrity": "sha512-EaV1Gl4mUEV4ddhDnv/xtj7sxwrwxdetHdWUGnT4VJQf+4d05v6lHYZr8N573k5Z0BViss7BDhfWtKS3+sfAqQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/proc-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", + "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/read-package-json": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-6.0.4.tgz", + "integrity": "sha512-AEtWXYfopBj2z5N5PbkAOeNHRPUg5q+Nen7QLxV8M2zJq1ym6/lCz3fYNTCXe19puu2d06jfHhrP7v/S2PtMMw==", + "dev": true, + "dependencies": { + "glob": "^10.2.2", + "json-parse-even-better-errors": "^3.0.0", + "normalize-package-data": "^5.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", + "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", + "dev": true, + "dependencies": { + "json-parse-even-better-errors": "^3.0.0", + "npm-normalize-package-bin": "^3.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json-fast/node_modules/json-parse-even-better-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", + "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/read-package-json/node_modules/json-parse-even-better-errors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.0.tgz", + "integrity": "sha512-iZbGHafX/59r39gPwVPRBGw0QQKnA7tte5pSMrhWOW7swGsVvVTjmfyAV9pNqk8YGT7tRCdxRu8uzcgZwoDooA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/reflect-metadata": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.1.13.tgz", + "integrity": "sha512-Ts1Y/anZELhSsjMcU605fU9RE4Oi3p5ORujwbIKXfWa+0Zxs510Qrmrce5/Jowq3cHSZSJqBjypxmHarc+vEWg==", + "dev": true + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.0.tgz", + "integrity": "sha512-d1VudCLoIGitcU/hEg2QqvyGZQmdC0Lf8BqdOMXGFSvJP4bNV1+XqbPQeHHLD51Jh4QJJ225dlIFvY4Ly6MXmQ==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.1.tgz", + "integrity": "sha512-knzmNAcuyxV+gQCufkYcvOqX/qIIfHLv0u5x79kRxuGojfYVky1f15TzZEu2Avte8QGepvUNTnLskf8E6X6Vyg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.2.11", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.2.11.tgz", + "integrity": "sha512-jbD/FT0+9MBU2XAZluI7w2OBs1RBi6p9M83nkoZayQXXU9e8Robt69FcZc7wU4eJD/YFTjn1JdCk3rbMJajz8Q==", + "dev": true + }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.2", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.2.tgz", + "integrity": "sha512-Sb+mjNHOULsBv818T40qSPeRiuWLyaGMa5ewydRLFimneixmVy2zdivRl+AF6jaYPC8ERxGDmFSiqui6SfPd+g==", + "dev": true, + "dependencies": { + "is-core-module": "^2.11.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", + "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/rollup": { + "version": "3.25.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.25.3.tgz", + "integrity": "sha512-ZT279hx8gszBj9uy5FfhoG4bZx8c+0A1sbqtr7Q3KNWIizpTdDEPZbV2xcbvHsnFp4MavCQYZyzApJ+virB8Yw==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/safevalues": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/safevalues/-/safevalues-0.3.4.tgz", + "integrity": "sha512-LRneZZRXNgjzwG4bDQdOTSbze3fHm1EAKN/8bePxnlEZiBmkYEDggaHbuvHI9/hoqHbGfsEA7tWS9GhYHZBBsw==" + }, + "node_modules/sass": { + "version": "1.63.2", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.63.2.tgz", + "integrity": "sha512-u56TU0AIFqMtauKl/OJ1AeFsXqRHkgO7nCWmHaDwfxDo9GUMSqBA4NEh6GMuh1CYVM7zuROYtZrHzPc2ixK+ww==", + "dev": true, + "dependencies": { + "chokidar": ">=3.0.0 <4.0.0", + "immutable": "^4.0.0", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/sass-loader": { + "version": "13.3.1", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-13.3.1.tgz", + "integrity": "sha512-cBTxmgyVA1nXPvIK4brjJMXOMJ2v2YrQEuHqLw3LylGb3gsR6jAvdjHMcy/+JGTmmIF9SauTrLLR7bsWDMWqgg==", + "dev": true, + "dependencies": { + "klona": "^2.0.6", + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "fibers": ">= 3.1.0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "fibers": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true, + "optional": true + }, + "node_modules/schema-utils": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.2.0.tgz", + "integrity": "sha512-L0jRsrPpjdckP3oPug3/VxNKt2trR8TcabrM6FOAAlvC/9Phcmm+cuAgTlxBqdBR1WJx7Naj9WHw+aOmheSVbw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.1.1.tgz", + "integrity": "sha512-GSL3aowiF7wa/WtSFwnUrludWFoNhftq8bUkH9pkzjpN2XSPOAYEgg6e0sS9s0rZwgJzJiQRPU18A6clnoW5wQ==", + "dev": true, + "dependencies": { + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.3.tgz", + "integrity": "sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dev": true, + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "dev": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.0.2.tgz", + "integrity": "sha512-MY2/qGx4enyjprQnFaZsHib3Yadh3IXyV2C321GY0pjGfVBu4un0uDJkwgdxqO+Rdx8JMT8IfJIRwbYVz3Ob3Q==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-1.6.0.tgz", + "integrity": "sha512-QODKff/qW/TXOZI6V/Clqu74xnInAS6it05mufj4/fSewexLtfEntgLZZcBtUK44CDQyUE5TUXYy1ARYzlfG9g==", + "dev": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.1.0", + "@sigstore/tuf": "^1.0.0", + "make-fetch-happen": "^11.0.1", + "tuf-js": "^1.1.3" + }, + "bin": { + "sigstore": "bin/sigstore.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.1.tgz", + "integrity": "sha512-W+utHys2w//dhFjy7iQQu9sGd3eokCjGbl2r59tyLqNiJJBdIebn3GAKEXBr3osqHTObJi2die/25bCx2zsaaw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "dev": true, + "dependencies": { + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.7.1.tgz", + "integrity": "sha512-7maUZy1N7uo6+WVEX6psASxtNlKaNVMlGQKkG/63nEDdLOWNbiUMoLK7X4uYoLhQstau72mLgfEWcXcwsaHbYQ==", + "dev": true, + "dependencies": { + "ip": "^2.0.0", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.13.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-7.0.0.tgz", + "integrity": "sha512-Fgl0YPZ902wEsAyiQ+idGd1A7rSFx/ayC1CQVMw5P+EQx2V0SgpGtf6OKFhVjPflPUl9YMmEOnmfjCdMUsygww==", + "dev": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-4.0.1.tgz", + "integrity": "sha512-oqXpzDIByKONVY8g1NUPOTQhe0UTU5bWUl32GSkqK2LjJj0HmwTMVKxcUip0RgAYhY1mqgOxjbQM48a0mmeNfA==", + "dev": true, + "dependencies": { + "abab": "^2.0.6", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "dev": true + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true + }, + "node_modules/ssri": { + "version": "10.0.4", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.4.tgz", + "integrity": "sha512-12+IR2CB2C28MMAw0Ncqwj5QbTcs0nGIhgJzYWzDkb21vWmfNI83KS4f3Ci6GI98WreIfG7o9UXp3C0qbpA8nQ==", + "dev": true, + "dependencies": { + "minipass": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.1.15", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.15.tgz", + "integrity": "sha512-/zKt9UyngnxIT/EAGYuxaMYgOIJiP81ab9ZfkILq4oNLPFX50qyYmu7jRj9qeXoxmJHjGlbH0+cm2uy1WCs10A==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/terser": { + "version": "5.17.7", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.7.tgz", + "integrity": "sha512-/bi0Zm2C6VAexlGgLlVxA0P2lru/sdLyfCVaRMfKVo9nWxbmz7f/sD8VPybPeSUJaJcwmCJis9pBIhcVcG1QcQ==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.0.tgz", + "integrity": "sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==" + }, + "node_modules/tuf-js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-1.1.7.tgz", + "integrity": "sha512-i3P9Kgw3ytjELUfpuKVDNBJvk4u5bXL6gskv572mcevPbSKCV3zt3djhmlEQ65yERjIbOSncy7U4cQJaB1CBCg==", + "dev": true, + "dependencies": { + "@tufjs/models": "1.0.4", + "debug": "^4.3.4", + "make-fetch-happen": "^11.1.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.35", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.35.tgz", + "integrity": "sha512-veRf7dawaj9xaWEu9HoTVn5Pggtc/qj+kqTOFvNiN1l0YdxwC1kvel57UCjThjGa3BHBihE8/UJAHI+uQHmd/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + } + ], + "engines": { + "node": "*" + } + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz", + "integrity": "sha512-dCwEFf0/oT85M1fHBg4F0jtLwJrutGoHSQXCh7u4o2t1drG+c0a9Flnqww6XUKSfQMPpJBRjU8d4RXB09qtvaA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", + "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", + "dev": true, + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.3.9.tgz", + "integrity": "sha512-qsTNZjO9NoJNW7KnOrgYwczm0WctJ8m/yqYAMAK9Lxt4SoySUfS5S8ia9K7JHpa3KEeMfyF8LoJ3c5NeBJy6pg==", + "dev": true, + "dependencies": { + "esbuild": "^0.17.5", + "postcss": "^8.4.23", + "rollup": "^3.21.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/webpack": { + "version": "5.86.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.86.0.tgz", + "integrity": "sha512-3BOvworZ8SO/D4GVP+GoRC3fVeg5MO4vzmq8TJJEkdmopxyazGDxN8ClqN12uzrZW9Tv8EED8v5VSb6Sqyi0pg==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.9.0", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.14.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-6.1.1.tgz", + "integrity": "sha512-y51HrHaFeeWir0YO4f0g+9GwZawuigzcAdRNon6jErXy/SqV/+O6eaVAzDqE6t3e3NpGeR5CS+cCDaTC+V3yEQ==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.12", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.0.tgz", + "integrity": "sha512-HmNB5QeSl1KpulTBQ8UT4FPrByYyaLxpJoQ0+s7EvUrMc16m0ZS1sgb1XGqzmgCPk0c9y+aaXxn11tbLzuM7NQ==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.1", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.3.tgz", + "integrity": "sha512-hj5CYrY0bZLB+eTO+x/j67Pkrquiy7kWepMHmUMoPsmcUaeEnQJqFzHJOyxgWlq746/wUuA64p9ta34Kyb01pA==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.13.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.13.0.tgz", + "integrity": "sha512-x9vcZYTrFPC7aSIbj7sRCYo7L/Xb8Iy+pW0ng0wt2vCJv7M9HOMy0UoN3rr+IFC7hb7vXoqS+P9ktyLLLhO+LA==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/webpack-merge": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.9.0.tgz", + "integrity": "sha512-6NbRQw4+Sy50vYNTw7EyOn41OZItPiXB8GNv3INSoe3PSFaHJEz3SHTrYVaRm2LilNGnFUzh0FAwqPEmU/CwDg==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "dev": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/zone.js": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.13.1.tgz", + "integrity": "sha512-+bIeDAFEBYuXRuU3qGQvzdPap+N1zjM4KkBAiiQuVVCrHrhjDuY6VkUhNa5+U27+9w0q3fbKiMCbpJ0XzMmSWA==", + "dependencies": { + "tslib": "^2.3.0" + } + } + } +} diff --git a/modules/ui/package.json b/modules/ui/package.json new file mode 100644 index 000000000..05add0189 --- /dev/null +++ b/modules/ui/package.json @@ -0,0 +1,42 @@ +{ + "name": "test-run-ui", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "test": "ng test", + "test:coverage": "ng test --code-coverage", + "docker": "docker rm -f test-run-ui && docker rmi test-run-ui && docker build -t test-run-ui . && docker run -d -p 80:80 --name test-run-ui test-run-ui" + }, + "private": true, + "dependencies": { + "@angular/animations": "^16.1.0", + "@angular/cdk": "^16.1.4", + "@angular/common": "^16.1.0", + "@angular/compiler": "^16.1.0", + "@angular/core": "^16.1.0", + "@angular/forms": "^16.1.0", + "@angular/material": "^16.1.4", + "@angular/platform-browser": "^16.1.0", + "@angular/platform-browser-dynamic": "^16.1.0", + "@angular/router": "^16.1.0", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.13.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^16.1.3", + "@angular/cli": "~16.1.3", + "@angular/compiler-cli": "^16.1.0", + "@types/jasmine": "~4.3.0", + "jasmine-core": "~4.6.0", + "karma": "~6.4.0", + "karma-chrome-launcher": "~3.2.0", + "karma-coverage": "~2.2.0", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "typescript": "~5.1.3" + } +} diff --git a/modules/ui/src/app/app-routing.module.ts b/modules/ui/src/app/app-routing.module.ts new file mode 100644 index 000000000..7624dca4b --- /dev/null +++ b/modules/ui/src/app/app-routing.module.ts @@ -0,0 +1,49 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {allowToRunTestGuard} from './guards/allow-to-run-test.guard'; +import { HashLocationStrategy, LocationStrategy } from '@angular/common'; + +const routes: Routes = [ + { + path: 'runtime', + canActivate: [allowToRunTestGuard], + loadChildren: () => import('./progress/progress.module').then(m => m.ProgressModule) + }, + { + path: 'device-repository', + loadChildren: () => import('./device-repository/device-repository.module').then(m => m.DeviceRepositoryModule) + }, + { + path: 'results', + canActivate: [allowToRunTestGuard], + loadChildren: () => import('./history/history.module').then(m => m.HistoryModule) + }, + { + path: '', + redirectTo: 'runtime', + pathMatch: 'full' + } +]; + +@NgModule({ + imports: [RouterModule.forRoot(routes)], + exports: [RouterModule], + providers: [{provide: LocationStrategy, useClass: HashLocationStrategy}] +}) +export class AppRoutingModule { +} diff --git a/modules/ui/src/app/app.component.html b/modules/ui/src/app/app.component.html new file mode 100644 index 000000000..d4632736c --- /dev/null +++ b/modules/ui/src/app/app.component.html @@ -0,0 +1,87 @@ + + + +
+ + + + + + + + +
+
+ + + +
+ + Testrun + + + + +
+ +
+ + + + + + + + + + + + + diff --git a/modules/ui/src/app/app.component.scss b/modules/ui/src/app/app.component.scss new file mode 100644 index 000000000..d0a1f2cd4 --- /dev/null +++ b/modules/ui/src/app/app.component.scss @@ -0,0 +1,119 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '@angular/material' as mat; +@import "../theming/colors"; + +.app-container { + height: 100%; +} + +.spacer { + flex: 1 1 auto; +} + +.mat-drawer-content { + background: $white; +} + +.mat-drawer-side { + border-right: none; +} + +.app-sidebar { + display: flex; + flex-direction: column; + background-color: $color-background-grey; + height: 100%; + gap: 8px; + width: 80px; + align-items: center; +} + +.app-sidebar-button, .app-toolbar-button { + border-radius: 20px; + border: 1px solid transparent; + min-width: 48px; + padding: 0; + box-sizing: border-box; + height: 34px; + margin: 6px 0; + line-height: 50%; +} + +.app-sidebar-button:disabled { + background: rgba(0, 0, 0, 0.12); +} + +.app-sidebar-button > .mat-icon, .app-toolbar-button > .mat-icon { + margin-right: 0; + width: 24px; + font-size: 24px; + color: $dark-grey; +} + +.app-sidebar-button > .mat-icon { + line-height: 18px; +} + +.app-sidebar-button-active { + border: 1px solid mat.get-color-from-palette($color-primary, 500); + background-color: mat.get-color-from-palette($color-primary, 500); +} + +.app-sidebar-button-active > .mat-icon { + color: $white; +} + +.logo-link { + color: $grey-800; + text-decoration: none; + font-size: 18px; + display: flex; + flex-wrap: nowrap; + align-items: center; + justify-content: center; + gap: 16px; +} + +.logo-link .mat-icon { + width: 36px; + height: 23px; +} + +.app-toolbar { + height: 56px; + padding: 0 6px 0 16px; + background-color: $white; + border-bottom: 1px solid $light-grey; + color: $grey-800; +} + +.app-content { + display: grid; + grid-template-rows: auto 1fr; +} + +.app-content-main { + display: grid; + grid-template-rows: 0 auto; + overflow: hidden; +} + +.settings-drawer { + width: 320px; + box-shadow: none; + border-left: 1px solid $light-grey; +} diff --git a/modules/ui/src/app/app.component.spec.ts b/modules/ui/src/app/app.component.spec.ts new file mode 100644 index 000000000..265cf12ec --- /dev/null +++ b/modules/ui/src/app/app.component.spec.ts @@ -0,0 +1,215 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {HttpClientTestingModule} from '@angular/common/http/testing'; +import {ComponentFixture, fakeAsync, TestBed, tick} from '@angular/core/testing'; +import {Router} from '@angular/router'; +import {RouterTestingModule} from '@angular/router/testing'; +import {AppComponent} from './app.component'; +import {TestRunService} from './test-run.service'; +import {BehaviorSubject} from 'rxjs/internal/BehaviorSubject'; +import {Device} from './model/device'; +import {device} from './mocks/device.mock'; +import {Component, EventEmitter, Output} from '@angular/core'; +import {MatButtonModule} from '@angular/material/button'; +import {MatIconModule} from '@angular/material/icon'; +import {MatToolbarModule} from '@angular/material/toolbar'; +import {MatSidenavModule} from '@angular/material/sidenav'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {AppRoutingModule} from './app-routing.module'; +import SpyObj = jasmine.SpyObj; + +describe('AppComponent', () => { + let component: AppComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + let router: Router; + let mockService: SpyObj; + + beforeEach(() => { + mockService = jasmine.createSpyObj(['getDevices', 'fetchDevices', 'getSystemStatus', 'fetchHistory']); + mockService.getDevices.and.returnValue(new BehaviorSubject([device])); + + TestBed.configureTestingModule({ + imports: [RouterTestingModule, HttpClientTestingModule, AppRoutingModule, MatButtonModule, + BrowserAnimationsModule, MatIconModule, MatToolbarModule, MatSidenavModule], + providers: [{provide: TestRunService, useValue: mockService}], + declarations: [AppComponent, FakeGeneralSettingsComponent] + }); + + fixture = TestBed.createComponent(AppComponent); + component = fixture.componentInstance; + router = TestBed.get(Router); + fixture.detectChanges(); + compiled = fixture.nativeElement as HTMLElement; + }); + + it('should create the app', () => { + const app = fixture.componentInstance; + expect(app).toBeTruthy(); + }); + + it('should render side bar', () => { + const sideBar = compiled.querySelector('.app-sidebar'); + + expect(sideBar).toBeDefined(); + }); + + it('should render menu button', () => { + const button = compiled.querySelector('.app-sidebar-button-menu'); + + expect(button).toBeDefined(); + }); + + it('should render runtime button', () => { + const button = compiled.querySelector('.app-sidebar-button-runtime'); + + expect(button).toBeDefined(); + }); + + it('should render device repository button', () => { + const button = compiled.querySelector( + '.app-sidebar-button-device-repository' + ); + + expect(button).toBeDefined(); + }); + + it('should render results button', () => { + const button = compiled.querySelector('.app-sidebar-button-results'); + + expect(button).toBeDefined(); + }); + + it('should render toolbar', () => { + const toolBar = compiled.querySelector('.app-toolbar'); + + expect(toolBar).toBeDefined(); + }); + + it('should render logo link', () => { + const logoLink = compiled.querySelector('.logo-link'); + + expect(logoLink).toBeDefined(); + }); + + it('should render general settings button', () => { + const generalSettingsButton = compiled.querySelector( + '.app-toolbar-button-general-settings' + ); + + expect(generalSettingsButton).toBeDefined(); + }); + + it('should navigate to the device repository when "device repository" button is clicked', fakeAsync(() => { + const button = compiled.querySelector( + '.app-sidebar-button-device-repository' + ) as HTMLButtonElement; + + button?.click(); + tick(); + + expect(router.url).toBe(`/device-repository`); + })); + + it('should navigate to the runtime when "runtime" button is clicked', fakeAsync(() => { + const button = compiled.querySelector( + '.app-sidebar-button-runtime' + ) as HTMLButtonElement; + + button?.click(); + tick(); + + expect(router.url).toBe(`/runtime`); + })); + + it('should navigate to the results when "results" button is clicked', fakeAsync(() => { + const button = compiled.querySelector( + '.app-sidebar-button-results' + ) as HTMLButtonElement; + + button?.click(); + tick(); + + expect(router.url).toBe(`/results`); + })); + + it('should call toggleSettingsBtn focus when settingsDrawer close on closeSetting', fakeAsync(() => { + spyOn(component.settingsDrawer, 'close').and.returnValue(Promise.resolve('close')); + spyOn(component.toggleSettingsBtn, 'focus'); + + component.closeSetting(); + tick(); + + component.settingsDrawer.close().then(() => { + expect(component.toggleSettingsBtn.focus).toHaveBeenCalled(); + } + ) + })); + + it('should call settingsDrawer open on openSetting', fakeAsync(() => { + spyOn(component.settingsDrawer, 'open'); + + component.openSetting(); + tick(); + + expect(component.settingsDrawer.open).toHaveBeenCalledTimes(1); + })); + + it('should call settingsDrawer toggle on click settings button', () => { + const settingsBtn = compiled.querySelector( + '.app-toolbar-button-general-settings' + ) as HTMLButtonElement; + spyOn(component.settingsDrawer, 'toggle'); + + settingsBtn.click(); + + expect(component.settingsDrawer.toggle).toHaveBeenCalledTimes(1); + }); + + describe('with no devices setted', () => { + beforeEach(() => { + mockService.getDevices.and.returnValue(new BehaviorSubject(null)); + component.ngOnInit(); + fixture.detectChanges(); + }); + + it('should have "results" and "runtime" buttons disabled', fakeAsync(() => { + const resultBtn = compiled.querySelector('.app-sidebar-button-results') as HTMLButtonElement; + const runtimeBtn = compiled.querySelector('.app-sidebar-button-runtime') as HTMLButtonElement; + + expect(resultBtn.disabled).toBe(true); + expect(runtimeBtn.disabled).toBe(true); + })); + + it('should have "device repository" button disabled', fakeAsync(() => { + const deviceRepositorytBtn = compiled.querySelector( + '.app-sidebar-button-device-repository' + ) as HTMLButtonElement; + + expect(deviceRepositorytBtn.disabled).toBe(false); + })); + }); + +}); + +@Component({ + selector: 'app-general-settings', + template: '
' +}) +class FakeGeneralSettingsComponent { + @Output() closeSettingEvent = new EventEmitter(); + @Output() openSettingEvent = new EventEmitter(); +} diff --git a/modules/ui/src/app/app.component.ts b/modules/ui/src/app/app.component.ts new file mode 100644 index 000000000..9b3f243e9 --- /dev/null +++ b/modules/ui/src/app/app.component.ts @@ -0,0 +1,80 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Component, OnInit, ViewChild} from '@angular/core'; +import {MatIconRegistry} from '@angular/material/icon'; +import {DomSanitizer} from '@angular/platform-browser'; +import {MatDrawer, MatDrawerToggleResult} from '@angular/material/sidenav'; +import {TestRunService} from './test-run.service'; +import {Observable} from 'rxjs/internal/Observable'; +import {Device} from './model/device'; + +const DEVICES_LOGO_URL = '/assets/icons/devices.svg'; +const REPORTS_LOGO_URL = '/assets/icons/reports.svg'; +const TESTRUN_LOGO_URL = '/assets/icons/testrun_logo_small.svg'; +const TESTRUN_LOGO_COLOR_URL = '/assets/icons/testrun_logo_color.svg'; +const CLOSE_URL = '/assets/icons/close.svg'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.scss'] +}) +export class AppComponent implements OnInit { + devices$!: Observable; + @ViewChild('settingsDrawer') public settingsDrawer!: MatDrawer; + @ViewChild('toggleSettingsBtn') public toggleSettingsBtn!: HTMLButtonElement; + + constructor( + private matIconRegistry: MatIconRegistry, + private domSanitizer: DomSanitizer, + private testRunService: TestRunService + ) { + testRunService.fetchDevices(); + this.matIconRegistry.addSvgIcon( + 'devices', + this.domSanitizer.bypassSecurityTrustResourceUrl(DEVICES_LOGO_URL) + ); + this.matIconRegistry.addSvgIcon( + 'reports', + this.domSanitizer.bypassSecurityTrustResourceUrl(REPORTS_LOGO_URL) + ); + this.matIconRegistry.addSvgIcon( + 'testrun_logo_small', + this.domSanitizer.bypassSecurityTrustResourceUrl(TESTRUN_LOGO_URL) + ); + this.matIconRegistry.addSvgIcon( + 'testrun_logo_color', + this.domSanitizer.bypassSecurityTrustResourceUrl(TESTRUN_LOGO_COLOR_URL) + ); + this.matIconRegistry.addSvgIcon( + 'close', + this.domSanitizer.bypassSecurityTrustResourceUrl(CLOSE_URL) + ); + } + + ngOnInit(): void { + this.devices$ = this.testRunService.getDevices(); + } + + async closeSetting(): Promise { + return await this.settingsDrawer.close().then(() => this.toggleSettingsBtn.focus()); + } + + async openSetting(): Promise { + return await this.settingsDrawer.open(); + } + +} diff --git a/modules/ui/src/app/app.module.ts b/modules/ui/src/app/app.module.ts new file mode 100644 index 000000000..89c2d3eb9 --- /dev/null +++ b/modules/ui/src/app/app.module.ts @@ -0,0 +1,55 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {HttpClientModule} from '@angular/common/http'; +import {NgModule} from '@angular/core'; +import {MatButtonModule} from '@angular/material/button'; +import {MatButtonToggleModule} from '@angular/material/button-toggle'; +import {MatIconModule} from '@angular/material/icon'; +import {MatSidenavModule} from '@angular/material/sidenav'; +import {MatToolbarModule} from '@angular/material/toolbar'; +import {MatRadioModule} from '@angular/material/radio'; +import {BrowserModule} from '@angular/platform-browser'; +import {NoopAnimationsModule} from '@angular/platform-browser/animations'; + +import {AppRoutingModule} from './app-routing.module'; +import {AppComponent} from './app.component'; +import {GeneralSettingsComponent} from './components/general-settings/general-settings.component'; +import {ReactiveFormsModule} from '@angular/forms'; +import {MatFormFieldModule} from '@angular/material/form-field'; +import {MatSnackBarModule} from '@angular/material/snack-bar'; + +@NgModule({ + declarations: [AppComponent, GeneralSettingsComponent], + imports: [ + BrowserModule, + AppRoutingModule, + NoopAnimationsModule, + MatButtonModule, + MatIconModule, + MatToolbarModule, + MatSidenavModule, + MatButtonToggleModule, + MatRadioModule, + HttpClientModule, + ReactiveFormsModule, + MatFormFieldModule, + MatSnackBarModule, + ], + providers: [], + bootstrap: [AppComponent] +}) +export class AppModule { +} diff --git a/modules/ui/src/app/components/device-item/device-item.component.html b/modules/ui/src/app/components/device-item/device-item.component.html new file mode 100644 index 000000000..535eb66c8 --- /dev/null +++ b/modules/ui/src/app/components/device-item/device-item.component.html @@ -0,0 +1,23 @@ + + diff --git a/modules/ui/src/app/components/device-item/device-item.component.scss b/modules/ui/src/app/components/device-item/device-item.component.scss new file mode 100644 index 000000000..51713ba78 --- /dev/null +++ b/modules/ui/src/app/components/device-item/device-item.component.scss @@ -0,0 +1,95 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../theming/colors"; +@import "../../../theming/variables"; + +$icon-width: 80px; +$border-radius: 12px; + +.device-item { + display: grid; + width: $device-item-width; + border-radius: $border-radius; + border: 1px solid #C4C7C5; + background: $white; + box-sizing: border-box; + grid-template-columns: 1fr 1fr $icon-width; + padding: 0; + grid-column-gap: 8px; + grid-row-gap: 4px; + font-family: 'Open Sans', sans-serif; + grid-template-areas: + "name name icon" + "manufacturer address icon"; + + &:hover { + cursor: pointer; + } +} + +.item-name { + padding: 0 16px; + grid-area: name; + justify-self: start; + align-self: end; + color: #1F1F1F; + justify-content: start; + font-size: 16px; + font-weight: 500; + line-height: 24px; + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + width: 230px; + text-align: start; +} + +.item-manufacturer { + padding: 0 16px; + grid-area: manufacturer; + justify-self: start; + color: $grey-800; + font-size: 14px; + font-style: normal; + font-weight: 400; + line-height: 20px; + max-width: 112px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.item-mac-address { + padding-right: 16px; + grid-area: address; + justify-self: end; + color: $grey-700; + font-family: Roboto, sans-serif; + font-size: 12px; + padding-top: 2px; + line-height: 20px; +} + +.item-icon { + grid-area: icon; + width: $icon-width; + height: calc($icon-width - 2px); + background-color: #E8F0FE; + justify-self: end; + border-top-right-radius: $border-radius; + border-bottom-right-radius: $border-radius; + background-image: url(/assets/icons/devices_add.svg); +} diff --git a/modules/ui/src/app/components/device-item/device-item.component.spec.ts b/modules/ui/src/app/components/device-item/device-item.component.spec.ts new file mode 100644 index 000000000..cb83ebb4b --- /dev/null +++ b/modules/ui/src/app/components/device-item/device-item.component.spec.ts @@ -0,0 +1,68 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {Device} from '../../model/device'; + +import {DeviceItemComponent} from './device-item.component'; +import {DeviceRepositoryModule} from '../../device-repository/device-repository.module'; + +describe('DeviceItemComponent', () => { + let component: DeviceItemComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [DeviceRepositoryModule, DeviceItemComponent] + }); + fixture = TestBed.createComponent(DeviceItemComponent); + component = fixture.componentInstance; + compiled = fixture.nativeElement as HTMLElement; + component.device = { + "manufacturer": "Delta", + "model": "O3-DIN-CPU", + "mac_addr": "00:1e:42:35:73:c4", + "test_modules": { + "dns": { + "enabled": true, + } + } + } as Device; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display information about device', () => { + const name = compiled.querySelector('.item-name'); + const manufacturer = compiled.querySelector('.item-manufacturer'); + const mac = compiled.querySelector('.item-mac-address'); + + expect(name?.textContent).toEqual("O3-DIN-CPU"); + expect(manufacturer?.textContent).toEqual("Delta"); + expect(mac?.textContent).toEqual("00:1e:42:35:73:c4"); + }); + + it('should emit mac address', () => { + const clickSpy = spyOn(component.itemClicked, 'emit'); + const item = compiled.querySelector('.device-item') as HTMLElement; + item.click(); + + expect(clickSpy).toHaveBeenCalledWith(component.device); + }); +}); diff --git a/modules/ui/src/app/components/device-item/device-item.component.ts b/modules/ui/src/app/components/device-item/device-item.component.ts new file mode 100644 index 000000000..757d4cbe1 --- /dev/null +++ b/modules/ui/src/app/components/device-item/device-item.component.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Component, EventEmitter, Input, Output} from '@angular/core'; +import {Device} from '../../model/device'; + +@Component({ + selector: 'app-device-item', + templateUrl: './device-item.component.html', + styleUrls: ['./device-item.component.scss'], + standalone: true +}) +export class DeviceItemComponent { + @Input() device!: Device; + @Output() itemClicked = new EventEmitter(); + + itemClick(): void { + this.itemClicked.emit(this.device); + } +} diff --git a/modules/ui/src/app/components/device-tests/device-tests.component.html b/modules/ui/src/app/components/device-tests/device-tests.component.html new file mode 100644 index 000000000..7c8cbbf28 --- /dev/null +++ b/modules/ui/src/app/components/device-tests/device-tests.component.html @@ -0,0 +1,25 @@ + +
+
+

Device options

+

+ + {{testModules[i].displayName}} + +

+
+
diff --git a/modules/ui/src/app/components/device-tests/device-tests.component.scss b/modules/ui/src/app/components/device-tests/device-tests.component.scss new file mode 100644 index 000000000..a37d0e320 --- /dev/null +++ b/modules/ui/src/app/components/device-tests/device-tests.component.scss @@ -0,0 +1,32 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../theming/colors"; + +:host { + overflow: auto; +} + +.disabled { + pointer-events: none; + opacity: 0.6; +} + +.device-tests-title { + margin-top: 20px; + font-size: 18px; + line-height: 24px; + color: $grey-800; +} diff --git a/modules/ui/src/app/components/device-tests/device-tests.component.spec.ts b/modules/ui/src/app/components/device-tests/device-tests.component.spec.ts new file mode 100644 index 000000000..b038d533e --- /dev/null +++ b/modules/ui/src/app/components/device-tests/device-tests.component.spec.ts @@ -0,0 +1,94 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {DeviceTestsComponent} from './device-tests.component'; +import {TestModule} from '../../model/device'; +import {FormArray, FormBuilder} from '@angular/forms'; + +describe('DeviceTestsComponent', () => { + let component: DeviceTestsComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [DeviceTestsComponent], + providers: [ + FormBuilder + ] + }); + fixture = TestBed.createComponent(DeviceTestsComponent); + component = fixture.componentInstance; + component.testModules = [ + { + displayName: "Connection", + name: "connection", + enabled: true + }, + { + displayName: "DNS", + name: "dns", + enabled: false + }, + ] as TestModule[]; + component.deviceForm = new FormBuilder().group({ + test_modules: new FormArray([]) + }); + fixture.detectChanges(); + compiled = fixture.nativeElement; + }); + + describe('component tests', () => { + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should fill tests with default values if device is not present', () => { + expect(component.test_modules.controls.length).toEqual(2); + expect(component.test_modules.controls[0].value).toEqual(true); + expect(component.test_modules.controls[1].value).toEqual(false); + }); + + it('should fill tests with device test values if device not present', () => { + component.deviceTestModules = { + "connection": { + "enabled": false, + }, + "dns": { + "enabled": true, + } + }; + component.ngOnInit(); + + expect(component.test_modules.controls[0].value).toEqual(false); + expect(component.test_modules.controls[1].value).toEqual(true); + }); + }) + + describe('DOM tests', () => { + it('should have checkboxes', () => { + const test = compiled.querySelectorAll('mat-checkbox input')!; + const testLabel = compiled.querySelectorAll('mat-checkbox label')!; + + expect(test.length).toEqual(2); + expect((test[0] as HTMLInputElement).checked).toBeTrue(); + expect((test[1] as HTMLInputElement).checked).toBeFalse(); + expect(testLabel[0].innerHTML.trim()).toEqual('Connection'); + expect(testLabel[1].innerHTML.trim()).toEqual('DNS'); + }); + }); +}); diff --git a/modules/ui/src/app/components/device-tests/device-tests.component.ts b/modules/ui/src/app/components/device-tests/device-tests.component.ts new file mode 100644 index 000000000..1d0475a0b --- /dev/null +++ b/modules/ui/src/app/components/device-tests/device-tests.component.ts @@ -0,0 +1,58 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ChangeDetectionStrategy, Component, Input, OnInit} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {FormArray, FormControl, FormGroup, ReactiveFormsModule} from '@angular/forms'; +import {TestModule, TestModules} from '../../model/device'; +import {MatCheckboxModule} from '@angular/material/checkbox'; + +@Component({ + selector: 'app-device-tests', + standalone: true, + imports: [CommonModule, MatCheckboxModule, ReactiveFormsModule,], + templateUrl: './device-tests.component.html', + styleUrls: ['./device-tests.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DeviceTestsComponent implements OnInit { + @Input() deviceForm!: FormGroup; + @Input() deviceTestModules?: TestModules | null; + @Input() testModules: TestModule[] = []; + // For initiate test run form tests should be displayed and disabled for change + @Input() disabled = false; + + get test_modules() { + return this.deviceForm?.controls['test_modules']! as FormArray; + } + + ngOnInit() { + this.fillTestModulesFormControls() + } + + fillTestModulesFormControls() { + this.test_modules.controls = []; + if (this.deviceTestModules) { + this.testModules.forEach(test => { + this.test_modules.push(new FormControl(this.deviceTestModules![test.name]?.enabled || false)); + }); + } else { + this.testModules.forEach(test => { + this.test_modules.push(new FormControl(test.enabled)); + }); + } + } + +} diff --git a/modules/ui/src/app/components/download-report/download-report.component.html b/modules/ui/src/app/components/download-report/download-report.component.html new file mode 100644 index 000000000..edb142486 --- /dev/null +++ b/modules/ui/src/app/components/download-report/download-report.component.html @@ -0,0 +1,26 @@ + + + + + + diff --git a/modules/ui/src/app/components/download-report/download-report.component.scss b/modules/ui/src/app/components/download-report/download-report.component.scss new file mode 100644 index 000000000..734e77089 --- /dev/null +++ b/modules/ui/src/app/components/download-report/download-report.component.scss @@ -0,0 +1,22 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +:host { + display: inline-block; +} + +.download-report-link { + display: inline-block; +} diff --git a/modules/ui/src/app/components/download-report/download-report.component.spec.ts b/modules/ui/src/app/components/download-report/download-report.component.spec.ts new file mode 100644 index 000000000..454ce564e --- /dev/null +++ b/modules/ui/src/app/components/download-report/download-report.component.spec.ts @@ -0,0 +1,113 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {DownloadReportComponent} from './download-report.component'; +import {MOCK_PROGRESS_DATA_COMPLIANT} from '../../mocks/progress.mock'; + +describe('DownloadReportComponent', () => { + let component: DownloadReportComponent; + let fixture: ComponentFixture; + + describe('Class tests', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [DownloadReportComponent] + }); + fixture = TestBed.createComponent(DownloadReportComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('#getTestRunId should return data for title of link', () => { + const expectedResult = 'Delta 03-DIN-CPU 1.2.2 22 Jun 2023 9:20'; + + const result = component.getTestRunId(MOCK_PROGRESS_DATA_COMPLIANT); + + expect(result).toEqual(expectedResult); + }); + + it('#getReportTitle should return data for download property of link', () => { + const expectedResult = 'delta_03-din-cpu_1.2.2_compliant_22_jun_2023_9:20'; + + const result = component.getReportTitle(MOCK_PROGRESS_DATA_COMPLIANT); + + expect(result).toEqual(expectedResult); + }); + + it('#getFormattedDateString should return date as string in the format "d MMM y H:mm"', () => { + const expectedResult = '22 Jun 2023 9:20'; + + const result = component.getFormattedDateString(MOCK_PROGRESS_DATA_COMPLIANT.started); + + expect(result).toEqual(expectedResult); + }); + + it('#getFormattedDateString should return empty string when no date', () => { + const expectedResult = ''; + + const result = component.getFormattedDateString(null); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('DOM tests', () => { + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [DownloadReportComponent] + }).compileComponents(); + fixture = TestBed.createComponent(DownloadReportComponent); + compiled = fixture.nativeElement as HTMLElement; + component = fixture.componentInstance; + }); + + describe('with not data provided', () => { + beforeEach(() => { + (component.data as any) = null; + fixture.detectChanges(); + }); + + it('should not have content', () => { + const downloadReportLink = compiled.querySelector('.download-report-link'); + + expect(downloadReportLink).toBeNull(); + }); + }); + + describe('with data provided', () => { + beforeEach(() => { + (component.data) = MOCK_PROGRESS_DATA_COMPLIANT; + fixture.detectChanges(); + }); + + it('should have download report link', () => { + const downloadReportLink = compiled.querySelector('.download-report-link') as HTMLAnchorElement; + + expect(downloadReportLink).not.toBeNull(); + expect(downloadReportLink.href).toEqual('https://api.testrun.io/report.pdf'); + expect(downloadReportLink.download).toEqual('delta_03-din-cpu_1.2.2_compliant_22_jun_2023_9:20'); + expect(downloadReportLink.title).toEqual('Download report for Test Run # Delta 03-DIN-CPU 1.2.2 22 Jun 2023 9:20'); + }); + }); + }); + +}); diff --git a/modules/ui/src/app/components/download-report/download-report.component.ts b/modules/ui/src/app/components/download-report/download-report.component.ts new file mode 100644 index 000000000..eb8e2e3b5 --- /dev/null +++ b/modules/ui/src/app/components/download-report/download-report.component.ts @@ -0,0 +1,47 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {TestrunStatus} from '../../model/testrun-status'; +import {CommonModule, DatePipe} from '@angular/common'; + +@Component({ + selector: 'app-download-report', + templateUrl: './download-report.component.html', + styleUrls: ['./download-report.component.scss'], + standalone: true, + imports: [CommonModule], + providers: [DatePipe], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class DownloadReportComponent { + @Input() data!: TestrunStatus; + + constructor(private datePipe: DatePipe) { + } + + getTestRunId(data: TestrunStatus) { + return `${data.device.manufacturer} ${data.device.model} ${data.device.firmware} ${this.getFormattedDateString(data.started)}`; + } + + getReportTitle(data: TestrunStatus) { + return `${data.device.manufacturer} ${data.device.model} ${data.device.firmware} ${data.status} ${this.getFormattedDateString(data.started)}`.replace(/ /g, "_").toLowerCase(); + } + + getFormattedDateString(date: string | null) { + return date ? this.datePipe.transform(date, 'd MMM y H:mm') : ''; + } + +} diff --git a/modules/ui/src/app/components/general-settings/general-settings.component.html b/modules/ui/src/app/components/general-settings/general-settings.component.html new file mode 100644 index 000000000..ff7301a5d --- /dev/null +++ b/modules/ui/src/app/components/general-settings/general-settings.component.html @@ -0,0 +1,75 @@ + +
+
+

Settings

+ +
+
+
+ + + + {{ interface }} + + + + + + {{ interface }} + + + + + Both interfaces must have different values + + +
+
+
+ + diff --git a/modules/ui/src/app/components/general-settings/general-settings.component.scss b/modules/ui/src/app/components/general-settings/general-settings.component.scss new file mode 100644 index 000000000..02dfcd107 --- /dev/null +++ b/modules/ui/src/app/components/general-settings/general-settings.component.scss @@ -0,0 +1,120 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '@angular/material' as mat; +@import "../../../theming/colors"; + +:host { + display: flex; + flex-direction: column; + height: 100%; +} + +.setting-container-content { + flex: 1 0 auto; +} + +.settings-drawer-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 12px 16px 24px; + + &-title { + margin: 0; + font-size: 22px; + font-style: normal; + font-weight: 400; + line-height: 28px; + } + + &-button { + min-width: 24px; + width: 24px; + height: 24px; + margin: 4px; + padding: 8px; + box-sizing: content-box; + + .close-button-icon { + width: 24px; + height: 24px; + margin: 0; + } + } +} + +.setting-drawer-content { + padding: 11px 16px 16px; +} + +.error-message-container { + display: block; +} + +.setting-form-label { + font-size: 18px; + + &.device-label { + display: inline-block; + padding-top: 16px; + } +} + +.setting-radio-group { + display: flex; + flex-direction: column; + margin-left: -10px; + align-items: flex-start; +} + +.setting-radio-button { + padding: 8px 0; + + ::ng-deep .mdc-form-field > label { + font-family: Roboto; + font-size: 16px; + font-style: normal; + font-weight: 400; + line-height: 24px; + letter-spacing: 0.1px; + color: $grey-800; + max-width: 240px; + overflow: hidden; + text-overflow: ellipsis; + } +} + +.setting-drawer-footer { + display: flex; + flex-shrink: 0; + justify-content: flex-end; + padding: 16px 24px 8px 24px; + + .close-button, .save-button { + padding: 0 24px; + font-size: 14px; + font-weight: 500; + line-height: 20px; + letter-spacing: 0.25px; + } + + .close-button { + margin-right: 10px; + &:enabled { + color: $secondary; + } + } +} diff --git a/modules/ui/src/app/components/general-settings/general-settings.component.spec.ts b/modules/ui/src/app/components/general-settings/general-settings.component.spec.ts new file mode 100644 index 000000000..b3cc6de6f --- /dev/null +++ b/modules/ui/src/app/components/general-settings/general-settings.component.spec.ts @@ -0,0 +1,141 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {GeneralSettingsComponent} from './general-settings.component'; +import {TestRunService} from '../../test-run.service'; +import {of} from 'rxjs'; +import {SystemConfig} from '../../model/setting'; +import {MatRadioModule} from '@angular/material/radio'; +import {ReactiveFormsModule} from '@angular/forms'; +import {MatButtonModule} from '@angular/material/button'; +import {MatIcon, MatIconModule} from '@angular/material/icon'; +import {MatIconTestingModule} from '@angular/material/icon/testing'; + +const MOCK_SYSTEM_CONFIG_EMPTY: SystemConfig = { + network: { + device_intf: '', + internet_intf: '' + } +} + +const MOCK_SYSTEM_CONFIG_WITH_DATA: SystemConfig = { + network: { + device_intf: 'mockDeviceValue', + internet_intf: 'mockInternetValue' + } +}; + +describe('GeneralSettingsComponent', () => { + let component: GeneralSettingsComponent; + let fixture: ComponentFixture; + let testRunServiceMock: jasmine.SpyObj; + + beforeEach(async () => { + testRunServiceMock = jasmine.createSpyObj(['getSystemInterfaces', 'getSystemConfig', 'setSystemConfig', 'createSystemConfig']); + testRunServiceMock.getSystemInterfaces.and.returnValue(of([])); + testRunServiceMock.getSystemConfig.and.returnValue(of(MOCK_SYSTEM_CONFIG_EMPTY)); + testRunServiceMock.createSystemConfig.and.returnValue(of({})); + + await TestBed.configureTestingModule({ + declarations: [GeneralSettingsComponent, MatIcon], + providers: [{provide: TestRunService, useValue: testRunServiceMock}], + imports: [MatButtonModule, MatIconModule, MatRadioModule, ReactiveFormsModule, MatIconTestingModule] + }).compileComponents(); + + fixture = TestBed.createComponent(GeneralSettingsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should call openSetting if not systemConfig data', () => { + spyOn(component.openSettingEvent, 'emit'); + + component.ngOnInit(); + + expect(component.openSettingEvent.emit).toHaveBeenCalled(); + }); + + it('should set default values to form if systemConfig data', () => { + testRunServiceMock.getSystemConfig.and.returnValue(of(MOCK_SYSTEM_CONFIG_WITH_DATA)); + + component.ngOnInit(); + + expect(component.deviceControl.value).toBe(MOCK_SYSTEM_CONFIG_WITH_DATA.network.device_intf); + expect(component.internetControl.value).toBe(MOCK_SYSTEM_CONFIG_WITH_DATA.network.internet_intf); + }); + + describe('#closeSetting', () => { + beforeEach(() => { + testRunServiceMock.systemConfig$ = of(MOCK_SYSTEM_CONFIG_WITH_DATA); + }); + + it('should emit closeSettingEvent', () => { + spyOn(component.closeSettingEvent, 'emit'); + + component.closeSetting(); + + expect(component.closeSettingEvent.emit).toHaveBeenCalled(); + }); + + it('should call reset settingForm', () => { + spyOn(component.settingForm, 'reset'); + + component.closeSetting(); + + expect(component.settingForm.reset).toHaveBeenCalled(); + }); + + it('should set value of settingForm on setSystemSetting', () => { + component.closeSetting(); + + expect(component.settingForm.value).toEqual(MOCK_SYSTEM_CONFIG_WITH_DATA.network); + }); + }); + + describe('#saveSetting', () => { + beforeEach(() => { + testRunServiceMock.systemConfig$ = of(MOCK_SYSTEM_CONFIG_WITH_DATA); + }); + + it('should have form error if form has the same value', () => { + const mockSameValue = 'sameValue'; + component.deviceControl.setValue(mockSameValue); + component.internetControl.setValue(mockSameValue); + + component.saveSetting(); + + expect(component.settingForm.invalid).toBeTrue(); + expect(component.isSubmitting).toBeTrue(); + expect(component.isFormError).toBeTrue(); + }); + + it('should call createSystemConfig when setting form valid', () => { + const {device_intf, internet_intf} = MOCK_SYSTEM_CONFIG_WITH_DATA.network; + component.deviceControl.setValue(device_intf); + component.internetControl.setValue(internet_intf); + + component.saveSetting(); + + expect(component.settingForm.invalid).toBeFalse(); + expect(testRunServiceMock.createSystemConfig).toHaveBeenCalledWith(MOCK_SYSTEM_CONFIG_WITH_DATA); + }); + }); +}); diff --git a/modules/ui/src/app/components/general-settings/general-settings.component.ts b/modules/ui/src/app/components/general-settings/general-settings.component.ts new file mode 100644 index 000000000..1d19ad462 --- /dev/null +++ b/modules/ui/src/app/components/general-settings/general-settings.component.ts @@ -0,0 +1,165 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Component, EventEmitter, OnDestroy, OnInit, Output} from '@angular/core'; +import {FormBuilder, FormControl, FormGroup, Validators} from '@angular/forms'; +import {Subject, takeUntil, tap} from 'rxjs'; +import {TestRunService} from '../../test-run.service'; +import {OnlyDifferentValuesValidator} from './only-different-values.validator'; + +@Component({ + selector: 'app-general-settings', + templateUrl: './general-settings.component.html', + styleUrls: ['./general-settings.component.scss'] +}) +export class GeneralSettingsComponent implements OnInit, OnDestroy { + @Output() closeSettingEvent = new EventEmitter(); + @Output() openSettingEvent = new EventEmitter(); + public readonly systemInterfaces$ = this.testRunService.getSystemInterfaces(); + public settingForm!: FormGroup; + public isSubmitting = false; + public hasSetting = false; + private destroy$: Subject = new Subject(); + + get deviceControl(): FormControl { + return this.settingForm.get('device_intf') as FormControl; + } + + get internetControl(): FormControl { + return this.settingForm.get('internet_intf') as FormControl; + } + + get isFormValues(): boolean { + return this.internetControl.value && this.deviceControl.value; + } + + get isFormError(): boolean { + return this.settingForm.hasError('hasSameValues'); + } + + constructor( + private readonly testRunService: TestRunService, + private readonly fb: FormBuilder, + private readonly onlyDifferentValuesValidator: OnlyDifferentValuesValidator + ) { + } + + ngOnInit() { + this.createSettingForm(); + + this.setSettingView(); + + this.cleanFormErrorMessage(); + } + + closeSetting(): void { + this.resetForm(); + this.closeSettingEvent.emit(); + this.setSystemSetting(); + } + + saveSetting(): void { + if (this.settingForm.invalid) { + this.isSubmitting = true; + this.settingForm.markAllAsTouched(); + } else { + this.createSystemConfig(); + } + } + + private createSettingForm(): FormGroup { + return this.settingForm = this.fb.group({ + device_intf: ['', Validators.required], + internet_intf: ['', Validators.required], + }, + { + validators: [this.onlyDifferentValuesValidator.onlyDifferentSetting()], + updateOn: 'change', + } + ) + } + + private setSettingView(): void { + this.testRunService.getSystemConfig() + .pipe(takeUntil(this.destroy$)) + .subscribe( + config => { + const {device_intf, internet_intf} = config.network; + if (device_intf && internet_intf) { + this.setDefaultFormValues(device_intf, internet_intf); + this.hasSetting = true; + } else { + this.openSetting(); + } + this.testRunService.setSystemConfig(config); + } + ); + } + + private setDefaultFormValues(device: string, internet: string): void { + this.deviceControl.setValue(device); + this.internetControl.setValue(internet); + } + + private cleanFormErrorMessage(): void { + this.settingForm.valueChanges + .pipe( + takeUntil(this.destroy$), + tap(() => this.isSubmitting = false), + ).subscribe(); + } + + private createSystemConfig(): void { + const {device_intf, internet_intf} = this.settingForm.value; + const data = { + network: { + device_intf, + internet_intf + } + } + + this.testRunService.createSystemConfig(data) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.closeSetting(); + this.testRunService.setSystemConfig(data); + this.hasSetting = true; + }); + } + + private openSetting(): void { + this.openSettingEvent.emit(); + } + + private setSystemSetting(): void { + this.testRunService.systemConfig$ + .pipe(takeUntil(this.destroy$)) + .subscribe(config => { + const {device_intf, internet_intf} = config.network; + if (device_intf && internet_intf) { + this.setDefaultFormValues(device_intf, internet_intf); + } + }) + } + + private resetForm(): void { + this.settingForm.reset(); + } + + ngOnDestroy() { + this.destroy$.next(true); + this.destroy$.unsubscribe(); + } +} diff --git a/modules/ui/src/app/components/general-settings/only-different-values.validator.ts b/modules/ui/src/app/components/general-settings/only-different-values.validator.ts new file mode 100644 index 000000000..3509062a1 --- /dev/null +++ b/modules/ui/src/app/components/general-settings/only-different-values.validator.ts @@ -0,0 +1,44 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Injectable} from '@angular/core'; +import {AbstractControl, FormControl, ValidationErrors, ValidatorFn} from '@angular/forms'; + +@Injectable({providedIn: 'root'}) + +export class OnlyDifferentValuesValidator { + public onlyDifferentSetting(): ValidatorFn { + return (form: AbstractControl): ValidationErrors | null => { + const deviceControl = form.get('device_intf') as FormControl; + const internetControl = form.get('internet_intf') as FormControl; + + if (!deviceControl || !internetControl) { + return null; + } + + const deviceControlValue = deviceControl.value; + const internetControlValue = internetControl.value; + + if (!deviceControlValue || !internetControlValue) { + return null; + } + + if (deviceControlValue === internetControlValue) { + return {'hasSameValues': true} + } + return null; + } + } +} diff --git a/modules/ui/src/app/device-repository/device-form/device-form.component.html b/modules/ui/src/app/device-repository/device-form/device-form.component.html new file mode 100644 index 000000000..e8245d74d --- /dev/null +++ b/modules/ui/src/app/device-repository/device-form/device-form.component.html @@ -0,0 +1,63 @@ + +
+ {{data.title}} + + Device Manufacturer + + Please enter device manufacturer name + + Device Manufacturer is required + + + + Device Model + + Please enter device name + + Device Model is required + + + + MAC address + + Please enter MAC address + + MAC address is required + + + Please, check. A MAC address consists of 12 hexadecimal digits (0 to 9, a to f, or A to F). + + + This MAC address is already used for another device in the repository. + + + + + + + {{error$| async}} + + + + + + +
diff --git a/modules/ui/src/app/device-repository/device-form/device-form.component.scss b/modules/ui/src/app/device-repository/device-form/device-form.component.scss new file mode 100644 index 000000000..1b3acc3c3 --- /dev/null +++ b/modules/ui/src/app/device-repository/device-form/device-form.component.scss @@ -0,0 +1,61 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../theming/colors"; + +$device-form-max-width: 549px; +$device-form-min-width: 285px; + +:host { + display: grid; + grid-template-rows: 1fr; + overflow: auto; + grid-template-columns: minmax(285px, $device-form-max-width); +} + +.device-form { + display: grid; + padding: 24px; + max-width: $device-form-max-width; + min-width: $device-form-min-width; + gap: 10px; + overflow: auto; +} + +.device-form-title { + color: $grey-800; + font-size: 22px; + line-height: 28px; + padding-bottom: 14px; +} + +.device-form-test-modules { + overflow: auto; + min-height: 78px; +} + +.device-form-actions { + padding: 0; + min-height: 30px; +} + +.close-button { + color: $grey-700; +} + +.device-form-mac-address-error { + white-space: nowrap; + margin-left: -22px; +} diff --git a/modules/ui/src/app/device-repository/device-form/device-form.component.spec.ts b/modules/ui/src/app/device-repository/device-form/device-form.component.spec.ts new file mode 100644 index 000000000..f9b2930b7 --- /dev/null +++ b/modules/ui/src/app/device-repository/device-form/device-form.component.spec.ts @@ -0,0 +1,390 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ComponentFixture, fakeAsync, flush, TestBed} from '@angular/core/testing'; + +import {DeviceFormComponent} from './device-form.component'; +import {TestRunService} from '../../test-run.service'; +import {MatButtonModule} from '@angular/material/button'; +import {FormControl, ReactiveFormsModule} from '@angular/forms'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatInputModule} from '@angular/material/input'; +import {MAT_DIALOG_DATA, MatDialogModule, MatDialogRef} from '@angular/material/dialog'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {Device} from '../../model/device'; +import {of, throwError} from 'rxjs'; +import {DeviceTestsComponent} from '../../components/device-tests/device-tests.component'; + +describe('DeviceFormComponent', () => { + let component: DeviceFormComponent; + let fixture: ComponentFixture; + let testRunServiceMock: jasmine.SpyObj; + let compiled: HTMLElement; + + beforeEach(() => { + testRunServiceMock = jasmine.createSpyObj(['getTestModules', 'hasDevice', 'saveDevice']); + testRunServiceMock.getTestModules.and.returnValue([ + { + displayName: "Connection", + name: "connection", + enabled: true + }, + { + displayName: "Smart Ready", + name: "udmi", + enabled: false + }, + ]); + TestBed.configureTestingModule({ + declarations: [DeviceFormComponent], + providers: [ + { + provide: TestRunService, + useValue: testRunServiceMock + }, + { + provide: MatDialogRef, + useValue: { + close: (result: any) => { + } + } + }, + {provide: MAT_DIALOG_DATA, useValue: {}},], + imports: [MatButtonModule, ReactiveFormsModule, MatCheckboxModule, MatInputModule, MatDialogModule, BrowserAnimationsModule, DeviceTestsComponent] + }); + fixture = TestBed.createComponent(DeviceFormComponent); + component = fixture.componentInstance; + compiled = fixture.nativeElement as HTMLElement; + component.data = {}; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should contain device form', () => { + const form = compiled.querySelector('.device-form'); + + expect(form).toBeTruthy(); + }); + + it('should close dialog on "cancel" click', () => { + const closeSpy = spyOn(component.dialogRef, 'close'); + const closeButton = compiled.querySelector('.close-button') as HTMLButtonElement; + + closeButton?.click(); + + expect(closeSpy).toHaveBeenCalledWith(); + + closeSpy.calls.reset(); + }); + + it('should not save data when fields are empty', () => { + const closeSpy = spyOn(component.dialogRef, 'close'); + const saveButton = compiled.querySelector('.save-button') as HTMLButtonElement; + const model: HTMLInputElement = compiled.querySelector('.device-form-model')!; + const manufacturer: HTMLInputElement = compiled.querySelector('.device-form-manufacturer')!; + const macAddress: HTMLInputElement = compiled.querySelector('.device-form-mac-address')!; + + ['', ' '].forEach(value => { + model.value = value; + model.dispatchEvent(new Event('input')); + manufacturer.value = value; + manufacturer.dispatchEvent(new Event('input')); + macAddress.value = value; + macAddress.dispatchEvent(new Event('input')); + saveButton?.click(); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const requiredErrors = compiled.querySelectorAll('mat-error')!; + expect(requiredErrors.length).toEqual(3); + + requiredErrors.forEach(error => { + expect(error?.innerHTML).toContain('required'); + }) + }); + + expect(closeSpy).not.toHaveBeenCalled(); + + closeSpy.calls.reset(); + }); + }); + + it('should not save data if no test selected', fakeAsync(() => { + const closeSpy = spyOn(component.dialogRef, 'close'); + component.model.setValue('model'); + component.manufacturer.setValue('manufacturer'); + component.mac_addr.setValue('07:07:07:07:07:07'); + component.test_modules.setValue([false, false]); + testRunServiceMock.hasDevice.and.returnValue(true); + + component.saveDevice(); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const error = compiled.querySelector('mat-error')!; + expect(error.innerHTML).toContain('At least one test has to be selected.'); + }); + + expect(closeSpy).not.toHaveBeenCalled(); + + closeSpy.calls.reset(); + flush(); + })); + + it('should not save data when server response with error', fakeAsync(() => { + const closeSpy = spyOn(component.dialogRef, 'close'); + component.model.setValue('model'); + component.manufacturer.setValue('manufacturer'); + component.mac_addr.setValue('07:07:07:07:07:07'); + testRunServiceMock.hasDevice.and.returnValue(false); + testRunServiceMock.saveDevice.and.returnValue(throwError({error: 'some error'})); + + component.saveDevice(); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const error = compiled.querySelector('mat-error')!; + expect(error.innerHTML).toContain('some error'); + }); + expect(closeSpy).not.toHaveBeenCalled(); + + closeSpy.calls.reset(); + flush(); + })); + + it('should save data when form is valid', () => { + const device: Device = { + "manufacturer": "manufacturer", + "model": "model", + "mac_addr": "07:07:07:07:07:07", + "test_modules": { + "connection": { + "enabled": true + }, + "udmi": { + "enabled": false + } + } + }; + const closeSpy = spyOn(component.dialogRef, 'close'); + component.model.setValue('model'); + component.manufacturer.setValue('manufacturer'); + component.mac_addr.setValue('07:07:07:07:07:07'); + testRunServiceMock.hasDevice.and.returnValue(false); + testRunServiceMock.saveDevice.and.returnValue(of(true)); + + component.saveDevice(); + + expect(closeSpy).toHaveBeenCalledTimes(1); + expect(closeSpy).toHaveBeenCalledWith(device); + + closeSpy.calls.reset(); + }); + + describe('test modules', () => { + it('should be present', () => { + const test = compiled.querySelectorAll('mat-checkbox'); + + expect(test.length).toEqual(2); + }); + + it('should be enabled', () => { + const testsForm = compiled.querySelector('app-device-tests form'); + + expect(testsForm?.classList.contains('disabled')).toEqual(false); + }); + }); + + describe('device model', () => { + it('should not contain errors when input is correct', fakeAsync(() => { + const model: HTMLInputElement = compiled.querySelector('.device-form-model')!; + ['model', 'Gebäude', 'jardín'].forEach(value => { + model.value = value; + model.dispatchEvent(new Event('input')); + + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const errors = component.model.errors; + const uiValue = model.value; + const formValue = component.model.value; + + expect(uiValue).toEqual(formValue); + expect(errors).toBeNull(); + }); + + flush(); + }); + + })); + }); + + describe('device manufacturer', () => { + it('should not contain errors when input is correct', fakeAsync(() => { + const manufacturer: HTMLInputElement = compiled.querySelector('.device-form-manufacturer')!; + ['manufacturer', 'Gebäude', 'jardín'].forEach(value => { + manufacturer.value = value; + manufacturer.dispatchEvent(new Event('input')); + + fixture.whenStable().then(() => { + const errors = component.manufacturer.errors; + const uiValue = manufacturer.value; + const formValue = component.manufacturer.value; + + expect(uiValue).toEqual(formValue); + expect(errors).toBeNull(); + }); + + flush(); + }) + })); + }); + + describe('mac address', () => { + it('should not be disabled', () => { + expect(component.mac_addr.disabled).toBeFalse(); + }); + + it('should not contain errors when input is correct', fakeAsync(() => { + const macAddress: HTMLInputElement = compiled.querySelector('.device-form-mac-address')!; + ['07:07:07:07:07:07', ' 07:07:07:07:07:07 '].forEach(value => { + macAddress.value = value; + macAddress.dispatchEvent(new Event('input')); + + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const errors = component.mac_addr.errors; + const uiValue = macAddress.value; + const formValue = component.mac_addr.value; + + expect(uiValue).toEqual(formValue); + expect(errors).toBeNull(); + }); + + flush(); + }) + })); + + it('should have "pattern" error when field does not satisfy pattern', fakeAsync(() => { + const macAddress: HTMLInputElement = compiled.querySelector('.device-form-mac-address')!; + ['value', '001e423573c4', ' '].forEach(value => { + macAddress.value = value; + macAddress.dispatchEvent(new Event('input')); + component.mac_addr.markAsTouched(); + + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const macAddressError = compiled.querySelector('mat-error')!.innerHTML; + const error = component.mac_addr.errors!['pattern']; + + expect(error).toBeTruthy(); + expect(macAddressError).toContain('Please, check. A MAC address consists of 12 hexadecimal digits (0 to 9, a to f, or A to F).'); + }); + + flush(); + }) + })); + + it('should have "has_same_mac_address" error when MAC address is already used', fakeAsync(() => { + testRunServiceMock.hasDevice.and.returnValue(true); + const macAddress: HTMLInputElement = compiled.querySelector('.device-form-mac-address')!; + macAddress.value = '07:07:07:07:07:07'; + macAddress.dispatchEvent(new Event('input')); + component.mac_addr.markAsTouched(); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const macAddressError = compiled.querySelector('mat-error')!.innerHTML; + const error = component.mac_addr.errors!['has_same_mac_address']; + + expect(error).toBeTruthy(); + expect(macAddressError).toContain('This MAC address is already used for another device in the repository.'); + }); + + flush(); + })); + }); + + describe('when device is present', () => { + beforeEach(() => { + component.data = { + device: { + "manufacturer": "Delta", + "model": "O3-DIN-CPU", + "mac_addr": "00:1e:42:35:73:c4", + "test_modules": { + "udmi": { + "enabled": true, + } + } + } + } + component.ngOnInit(); + fixture.detectChanges(); + }); + + it('should fill form values with device values', () => { + const model: HTMLInputElement = compiled.querySelector('.device-form-model')!; + const manufacturer: HTMLInputElement = compiled.querySelector('.device-form-manufacturer')!; + const macAddress: HTMLInputElement = compiled.querySelector('.device-form-mac-address')!; + + expect(model.value).toEqual('O3-DIN-CPU'); + expect(manufacturer.value).toEqual('Delta'); + expect(macAddress.value).toEqual('00:1e:42:35:73:c4'); + }); + + it('should save data even mac address already exist', fakeAsync(() => { + const closeSpy = spyOn(component.dialogRef, 'close'); + testRunServiceMock.saveDevice.and.returnValue(of(true)); + testRunServiceMock.hasDevice.and.returnValue(true); + // fill the test controls + component.test_modules.push(new FormControl(false)); + component.test_modules.push(new FormControl(true)); + component.saveDevice(); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const error = compiled.querySelector('mat-error')!; + expect(error).toBeFalse(); + }); + + expect(closeSpy).toHaveBeenCalledWith({ + "manufacturer": "Delta", + "model": "O3-DIN-CPU", + "mac_addr": "00:1e:42:35:73:c4", + "test_modules": { + "connection": { + "enabled": false, + }, + "udmi": { + "enabled": true, + } + } + }); + + closeSpy.calls.reset(); + flush(); + })); + + it('should disable mac address', () => { + expect(component.mac_addr.disabled).toBeTrue(); + }); + }); +}); diff --git a/modules/ui/src/app/device-repository/device-form/device-form.component.ts b/modules/ui/src/app/device-repository/device-form/device-form.component.ts new file mode 100644 index 000000000..42eaf44dd --- /dev/null +++ b/modules/ui/src/app/device-repository/device-form/device-form.component.ts @@ -0,0 +1,165 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Component, Inject, OnDestroy, OnInit} from '@angular/core'; +import {AbstractControl, FormArray, FormBuilder, FormGroup, Validators} from '@angular/forms'; +import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog'; +import {Device, TestModule} from '../../model/device'; +import {TestRunService} from '../../test-run.service'; +import {DeviceValidators} from './device.validators'; +import {catchError, of, retry, Subject, takeUntil} from 'rxjs'; +import {BehaviorSubject} from 'rxjs/internal/BehaviorSubject'; + +const MAC_ADDRESS_PATTERN = '^[\\s]*[a-fA-F0-9]{2}(?:[:][a-fA-F0-9]{2}){5}[\\s]*$'; + +interface DialogData { + title?: string; + device?: Device; +} + +@Component({ + selector: 'app-device-form', + templateUrl: './device-form.component.html', + styleUrls: ['./device-form.component.scss'] +}) +export class DeviceFormComponent implements OnInit, OnDestroy { + deviceForm!: FormGroup; + testModules: TestModule[] = []; + error$: BehaviorSubject = new BehaviorSubject(null); + private destroy$: Subject = new Subject(); + + constructor( + public dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) public data: DialogData, + private fb: FormBuilder, + private testRunService: TestRunService, + private deviceValidators: DeviceValidators, + ) { + } + + get model() { + return this.deviceForm.get('model')!; + } + + get manufacturer() { + return this.deviceForm.get('manufacturer')!; + } + + get mac_addr() { + return this.deviceForm.get('mac_addr')!; + } + + get test_modules() { + return this.deviceForm.controls['test_modules']! as FormArray; + } + + ngOnInit() { + this.createDeviceForm(); + this.testModules = this.testRunService.getTestModules(); + if (this.data.device) { + this.model.setValue(this.data.device.model); + this.manufacturer.setValue(this.data.device.manufacturer); + this.mac_addr.setValue(this.data.device.mac_addr); + } + } + + ngOnDestroy() { + this.destroy$.next(true); + this.destroy$.unsubscribe(); + } + + cancel(): void { + this.dialogRef.close(); + } + + saveDevice() { + this.checkMandatoryFields(); + if (this.deviceForm.invalid) { + this.deviceForm.markAllAsTouched(); + return; + } + + if (this.isAllTestsDisabled()) { + this.error$.next('At least one test has to be selected.'); + return; + } + + const device = this.createDeviceFromForm(); + + this.testRunService.saveDevice(device) + .pipe( + takeUntil(this.destroy$), + retry(1), + catchError(error => { + this.error$.next(error.error); + return of(null); + })) + .subscribe((deviceSaved: boolean | null) => { + if (deviceSaved) { + this.dialogRef.close(device); + } + }); + } + + private isAllTestsDisabled(): boolean { + return this.deviceForm.value.test_modules.every((enabled: boolean) => { + return !enabled; + }); + } + + private createDeviceFromForm(): Device { + const testModules: { [key: string]: { enabled: boolean } } = {}; + this.deviceForm.value.test_modules.forEach((enabled: boolean, i: number) => { + testModules[this.testModules[i]?.name] = { + enabled: enabled + } + }); + return { + model: this.model.value.trim(), + manufacturer: this.manufacturer.value.trim(), + mac_addr: this.mac_addr.value.trim(), + test_modules: testModules + } as Device; + } + + /** + * Model, manufacturer, MAC address are mandatory. + * It should be checked on submit. Other validation happens on blur. + */ + private checkMandatoryFields() { + this.setRequiredErrorIfEmpty(this.model); + this.setRequiredErrorIfEmpty(this.manufacturer); + this.setRequiredErrorIfEmpty(this.mac_addr); + } + + private setRequiredErrorIfEmpty(control: AbstractControl) { + if (!control.value.trim()) { + control.setErrors({required: true}); + } + } + + private createDeviceForm() { + this.deviceForm = this.fb.group({ + model: ['', [this.deviceValidators.deviceStringFormat()]], + manufacturer: ['', [this.deviceValidators.deviceStringFormat()]], + mac_addr: [ + {value: '', disabled: this.data.device}, [ + Validators.pattern(MAC_ADDRESS_PATTERN), + this.deviceValidators.differentMACAddress(this.data.device) + ]], + test_modules: new FormArray([]) + }); + } +} diff --git a/modules/ui/src/app/device-repository/device-form/device.validators.ts b/modules/ui/src/app/device-repository/device-form/device.validators.ts new file mode 100644 index 000000000..7be18e917 --- /dev/null +++ b/modules/ui/src/app/device-repository/device-form/device.validators.ts @@ -0,0 +1,54 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Injectable} from '@angular/core'; +import {AbstractControl, ValidationErrors, ValidatorFn} from '@angular/forms'; +import {TestRunService} from '../../test-run.service'; +import {Device} from '../../model/device'; + +@Injectable({providedIn: 'root'}) + +/** + * Validator uses for Device Name and Device Manufacturer inputs + */ +export class DeviceValidators { + + constructor(private testRunService: TestRunService) { + } + + readonly STRING_FORMAT_REGEXP = new RegExp('^([a-z0-9\\p{L}\\p{M}.\',-_ ]{1,64})$', 'u'); + + public deviceStringFormat(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value?.trim(); + if (value) { + let result = this.STRING_FORMAT_REGEXP.test(value); + return !result ? {'invalid_format': true} : null; + } + return null; + } + } + + public differentMACAddress(device?: Device): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const value = control.value?.trim(); + if (value && !device) { + let result = this.testRunService.hasDevice(value) + return result ? {'has_same_mac_address': true} : null; + } + return null; + } + } +} diff --git a/modules/ui/src/app/device-repository/device-repository-routing.module.ts b/modules/ui/src/app/device-repository/device-repository-routing.module.ts new file mode 100644 index 000000000..aca5be67d --- /dev/null +++ b/modules/ui/src/app/device-repository/device-repository-routing.module.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {DeviceRepositoryComponent} from './device-repository.component'; + +const routes: Routes = [{path: '', component: DeviceRepositoryComponent}]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class DeviceRepositoryRoutingModule { +} diff --git a/modules/ui/src/app/device-repository/device-repository.component.html b/modules/ui/src/app/device-repository/device-repository.component.html new file mode 100644 index 000000000..16dac47bf --- /dev/null +++ b/modules/ui/src/app/device-repository/device-repository.component.html @@ -0,0 +1,40 @@ + + + +

Device Repository

+ +
+
+ + + +
+
+ + +
+ +
+
+ + + + diff --git a/modules/ui/src/app/device-repository/device-repository.component.scss b/modules/ui/src/app/device-repository/device-repository.component.scss new file mode 100644 index 000000000..1dcf8f80e --- /dev/null +++ b/modules/ui/src/app/device-repository/device-repository.component.scss @@ -0,0 +1,46 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../theming/colors"; +@import "../../theming/variables"; + +:host { + overflow: hidden; + flex-direction: column; + display: flex; +} + +.device-repository-content-empty { + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.device-repository-toolbar { + padding-left: 40px; + gap: 16px; + background: $white; + height: 72px; +} + +.device-repository-content { + align-content: start; + padding: 24px; + display: grid; + grid-template-columns: repeat(auto-fit, $device-item-width); + gap: 16px; + overflow-y: auto; +} diff --git a/modules/ui/src/app/device-repository/device-repository.component.spec.ts b/modules/ui/src/app/device-repository/device-repository.component.spec.ts new file mode 100644 index 000000000..357c17d29 --- /dev/null +++ b/modules/ui/src/app/device-repository/device-repository.component.spec.ts @@ -0,0 +1,189 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ComponentFixture, fakeAsync, TestBed} from '@angular/core/testing'; +import {of} from 'rxjs'; +import {Device} from '../model/device'; +import {TestRunService} from '../test-run.service'; + +import {DeviceRepositoryComponent} from './device-repository.component'; +import {DeviceRepositoryModule} from './device-repository.module'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {DeviceFormComponent} from './device-form/device-form.component'; +import {MatDialogRef} from '@angular/material/dialog'; +import {BehaviorSubject} from 'rxjs/internal/BehaviorSubject'; +import {device} from '../mocks/device.mock'; +import SpyObj = jasmine.SpyObj; + +describe('DeviceRepositoryComponent', () => { + let service: TestRunService; + let component: DeviceRepositoryComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + let mockService: SpyObj; + + beforeEach(() => { + mockService = jasmine.createSpyObj(['getDevices', 'fetchDevices', 'setDevices', 'getTestModules', 'addDevice', 'updateDevice']); + mockService.getDevices.and.returnValue(new BehaviorSubject([])); + mockService.getTestModules.and.returnValue([ + { + displayName: "Connection", + name: "connection", + enabled: true + }, + { + displayName: "Smart Ready", + name: "udmi", + enabled: false + }, + ]); + + TestBed.configureTestingModule({ + imports: [DeviceRepositoryModule, BrowserAnimationsModule], + providers: [{provide: TestRunService, useValue: mockService}], + declarations: [DeviceRepositoryComponent] + }); + fixture = TestBed.createComponent(DeviceRepositoryComponent); + component = fixture.componentInstance; + compiled = fixture.nativeElement as HTMLElement; + service = fixture.debugElement.injector.get(TestRunService); + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('with no devices devices', () => { + beforeEach(() => { + mockService.getDevices = jasmine.createSpy().and.returnValue(of([])); + component.ngOnInit(); + }); + + it('should show only add device button if no device added', () => { + fixture.detectChanges(); + const button = compiled.querySelector('.device-repository-content-empty button'); + + expect(button).toBeTruthy(); + }); + }); + + describe('with devices', () => { + beforeEach(() => { + mockService.getDevices.and.returnValue(new BehaviorSubject([device, device, device])); + component.ngOnInit(); + }); + + it('should show device item', fakeAsync(() => { + fixture.detectChanges(); + const item = compiled.querySelectorAll('app-device-item'); + + expect(item.length).toEqual(3); + })); + + it('should open device dialog on item click', () => { + const openSpy = spyOn(component.dialog, 'open').and + .returnValue({ + afterClosed: () => of(true) + } as MatDialogRef); + fixture.detectChanges(); + + component.openDialog(device); + + expect(openSpy).toHaveBeenCalled(); + expect(openSpy).toHaveBeenCalledWith(DeviceFormComponent, { + data: { + device: device, + title: 'Edit device' + }, + autoFocus: true, + hasBackdrop: true, + disableClose: true, + panelClass: 'device-form-dialog' + }); + + openSpy.calls.reset(); + }); + }); + + it('should open device dialog on "add device button click"', () => { + const openSpy = spyOn(component.dialog, 'open').and + .returnValue({ + afterClosed: () => of(true) + } as MatDialogRef); + fixture.detectChanges(); + const button = compiled.querySelector('.device-repository-content-empty button') as HTMLButtonElement; + button?.click(); + + expect(button).toBeTruthy(); + expect(openSpy).toHaveBeenCalled(); + expect(openSpy).toHaveBeenCalledWith(DeviceFormComponent, { + data: {device: null, title: 'Create device'}, + autoFocus: true, + hasBackdrop: true, + disableClose: true, + panelClass: 'device-form-dialog' + }); + + openSpy.calls.reset(); + }); + + it('should not add device if dialog closes with null', () => { + spyOn(component.dialog, 'open').and + .returnValue({ + afterClosed: () => of(null) + } as MatDialogRef); + mockService.addDevice.and.callThrough(); + + component.openDialog(); + + expect(mockService.addDevice).not.toHaveBeenCalled(); + }); + + it('should add device if dialog closes with object', () => { + const device = { + "manufacturer": "Delta", + "model": "O3-DIN-CPU", + "mac_addr": "00:1e:42:35:73:c4", + "test_modules": { + "dns": { + "enabled": false + }, + "connection": { + "enabled": true + }, + "ntp": { + "enabled": false + }, + "baseline": { + "enabled": false + }, + "nmap": { + "enabled": false + } + } + } as Device; + spyOn(component.dialog, 'open').and + .returnValue({ + afterClosed: () => of(device) + } as MatDialogRef); + mockService.addDevice.and.callThrough(); + + component.openDialog(); + + expect(mockService.addDevice).toHaveBeenCalledWith(device); + }); + +}); diff --git a/modules/ui/src/app/device-repository/device-repository.component.ts b/modules/ui/src/app/device-repository/device-repository.component.ts new file mode 100644 index 000000000..54fb8abeb --- /dev/null +++ b/modules/ui/src/app/device-repository/device-repository.component.ts @@ -0,0 +1,68 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Component, OnInit} from '@angular/core'; +import {MatDialog} from '@angular/material/dialog'; +import {Observable} from 'rxjs/internal/Observable'; +import {Device} from '../model/device'; +import {TestRunService} from '../test-run.service'; +import {DeviceFormComponent} from './device-form/device-form.component'; +import {Subject, takeUntil} from 'rxjs'; + +@Component({ + selector: 'app-device-repository', + templateUrl: './device-repository.component.html', + styleUrls: ['./device-repository.component.scss'], +}) +export class DeviceRepositoryComponent implements OnInit { + devices$!: Observable; + private destroy$: Subject = new Subject(); + + constructor(private testRunService: TestRunService, public dialog: MatDialog) { + } + + ngOnInit(): void { + this.devices$ = this.testRunService.getDevices(); + } + + ngOnDestroy() { + this.destroy$.next(true); + this.destroy$.unsubscribe(); + } + + openDialog(selectedDevice?: Device): void { + const dialogRef = this.dialog.open(DeviceFormComponent, { + data: { + device: selectedDevice || null, + title: selectedDevice ? 'Edit device' : 'Create device' + }, + autoFocus: true, + hasBackdrop: true, + disableClose: true, + panelClass: 'device-form-dialog' + }); + + dialogRef?.afterClosed() + .pipe(takeUntil(this.destroy$)) + .subscribe(device => { + if (!selectedDevice && device) { + this.testRunService.addDevice(device); + } + if (selectedDevice && device) { + this.testRunService.updateDevice(selectedDevice, device); + } + }); + } +} diff --git a/modules/ui/src/app/device-repository/device-repository.module.ts b/modules/ui/src/app/device-repository/device-repository.module.ts new file mode 100644 index 000000000..002a8ca2c --- /dev/null +++ b/modules/ui/src/app/device-repository/device-repository.module.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ScrollingModule} from '@angular/cdk/scrolling'; +import {CommonModule} from '@angular/common'; +import {HttpClientModule} from '@angular/common/http'; +import {NgModule} from '@angular/core'; +import {ReactiveFormsModule} from '@angular/forms'; +import {MatButtonModule} from '@angular/material/button'; +import {MatCheckboxModule} from '@angular/material/checkbox'; +import {MatDialogModule} from '@angular/material/dialog'; +import {MatIconModule} from '@angular/material/icon'; +import {MatInputModule} from '@angular/material/input'; +import {MatToolbarModule} from '@angular/material/toolbar'; +import {DeviceFormComponent} from './device-form/device-form.component'; + +import {DeviceRepositoryRoutingModule} from './device-repository-routing.module'; +import {DeviceRepositoryComponent} from './device-repository.component'; +import {DeviceItemComponent} from '../components/device-item/device-item.component'; +import {DeviceTestsComponent} from '../components/device-tests/device-tests.component'; + +@NgModule({ + declarations: [ + DeviceRepositoryComponent, + DeviceFormComponent, + ], + imports: [ + CommonModule, + DeviceRepositoryRoutingModule, + MatToolbarModule, + MatButtonModule, + MatIconModule, + ScrollingModule, + HttpClientModule, + MatDialogModule, + ReactiveFormsModule, + MatCheckboxModule, + MatInputModule, + DeviceItemComponent, + DeviceTestsComponent, + ], +}) +export class DeviceRepositoryModule { +} diff --git a/modules/ui/src/app/guards/allow-to-run-test.guard.spec.ts b/modules/ui/src/app/guards/allow-to-run-test.guard.spec.ts new file mode 100644 index 000000000..b200a9d4f --- /dev/null +++ b/modules/ui/src/app/guards/allow-to-run-test.guard.spec.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {TestBed} from '@angular/core/testing'; +import {Router} from '@angular/router'; + +import {allowToRunTestGuard} from './allow-to-run-test.guard'; +import {TestRunService} from '../test-run.service'; +import {Device} from '../model/device'; +import {BehaviorSubject} from 'rxjs/internal/BehaviorSubject'; +import {device} from '../mocks/device.mock'; + +describe('allowToRunTestGuard', () => { + const mockRouter = jasmine.createSpyObj(['parseUrl']) + + const setup = (testRunServiceMock: unknown) => { + TestBed.configureTestingModule({ + providers: [ + allowToRunTestGuard, + {provide: TestRunService, useValue: testRunServiceMock}, + {provide: Router, useValue: mockRouter} + ] + }); + + return TestBed.runInInjectionContext(allowToRunTestGuard); + } + + it('should allow to continue', () => { + const mockTestRunService: unknown = {getDevices: () => new BehaviorSubject([device])} + + const guard = setup(mockTestRunService) + + guard.subscribe(res => { + expect(res).toBeTrue(); + }); + }); + + it('should redirect to the "/device-repository" path', () => { + + const mockTestRunService: unknown = {getDevices: () => new BehaviorSubject([])} + + const guard = setup(mockTestRunService) + + guard.subscribe(res => { + expect(res).toBeFalsy(); + expect(mockRouter.parseUrl).toHaveBeenCalledWith('/device-repository'); + }); + }); +}); diff --git a/modules/ui/src/app/guards/allow-to-run-test.guard.ts b/modules/ui/src/app/guards/allow-to-run-test.guard.ts new file mode 100644 index 000000000..5b6fe33e5 --- /dev/null +++ b/modules/ui/src/app/guards/allow-to-run-test.guard.ts @@ -0,0 +1,33 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {inject} from '@angular/core'; +import {TestRunService} from '../test-run.service'; +import {Router} from '@angular/router'; +import {Device} from '../model/device'; +import {map} from 'rxjs'; + +export const allowToRunTestGuard = () => { + const testRunService = inject(TestRunService); + const router = inject(Router); + + return testRunService.getDevices().pipe( + map((devices: Device[] | null) => { + return !(devices?.length) + ? router.parseUrl('/device-repository') + : !!devices; + }), + ); +}; diff --git a/modules/ui/src/app/history/history-routing.module.ts b/modules/ui/src/app/history/history-routing.module.ts new file mode 100644 index 000000000..cd2c77e4a --- /dev/null +++ b/modules/ui/src/app/history/history-routing.module.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {HistoryComponent} from './history.component'; + +const routes: Routes = [{path: '', component: HistoryComponent}]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class HistoryRoutingModule { +} diff --git a/modules/ui/src/app/history/history.component.html b/modules/ui/src/app/history/history.component.html new file mode 100644 index 000000000..e3a8840c3 --- /dev/null +++ b/modules/ui/src/app/history/history.component.html @@ -0,0 +1,82 @@ + + + +

Results

+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Started{{getFormattedDateString(data.started)}}Duration{{getDuration(data.started, data.finished)}}Device {{data.device.manufacturer}} {{data.device.model}} Firmware {{data.device.firmware}} Result + + {{data.status}} + + Download + + + file_download + + +
+
+
+ + +
+
+ Sorry, there are no reports yet! + Reports will automatically generate following a test run completion. +
+
+
diff --git a/modules/ui/src/app/history/history.component.scss b/modules/ui/src/app/history/history.component.scss new file mode 100644 index 000000000..26be45926 --- /dev/null +++ b/modules/ui/src/app/history/history.component.scss @@ -0,0 +1,98 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../theming/colors"; +@import "../../theming/variables"; + +:host { + overflow: hidden; + display: grid; + grid-template-rows: auto 1fr; +} + +.history-toolbar { + padding-left: 32px; + gap: 10px; + background: $white; + height: 89px; +} + +.history-content { + margin: 0 32px 39px 32px; + overflow-y: auto; + border-radius: 4px; + border: 1px solid $lighter-grey; + height: -webkit-fit-content; + height: -moz-fit-content; + height: fit-content; + max-height: -webkit-fill-available; + max-height: -moz-available; + max-height: stretch; +} + +.history-content table { + th { + font-weight: 700; + } + + td { + font-weight: 400; + } + + th, td { + font-family: Roboto, sans-serif; + font-size: 14px; + line-height: 20px; + letter-spacing: 0.2px; + } + + .table-cell-report { + padding-left: 36px; + } +} + +.results-content-empty { + display: flex; + align-items: center; + justify-content: center; + grid-row: 1/3; +} + +.results-content-empty-message { + display: flex; + flex-direction: column; + align-items: center; + gap: 16px; +} + +.results-content-empty-message-header { + font-weight: 400; + line-height: 28px; + font-size: 22px; + color: $black; +} + +.results-content-empty-message-main { + font-family: Roboto, sans-serif; + font-weight: 400; + font-size: 16px; + line-height: 24px; + letter-spacing: 0.1px; + color: #202124; +} + +.download-report-icon { + color: $dark-grey; +} diff --git a/modules/ui/src/app/history/history.component.spec.ts b/modules/ui/src/app/history/history.component.spec.ts new file mode 100644 index 000000000..2d4b29959 --- /dev/null +++ b/modules/ui/src/app/history/history.component.spec.ts @@ -0,0 +1,152 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {HistoryComponent} from './history.component'; +import {TestRunService} from '../test-run.service'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {HistoryModule} from './history.module'; +import {of} from 'rxjs'; +import {TestrunStatus} from '../model/testrun-status'; +import SpyObj = jasmine.SpyObj; + +const history = [{ + "status": "compliant", + "device": { + "manufacturer": "Delta", + "model": "03-DIN-SRC", + "mac_addr": "01:02:03:04:05:06", + "firmware": "1.2.2" + }, + "report": "https://api.testrun.io/report.pdf", + "started": "2023-06-23T10:11:00.123Z", + "finished": "2023-06-23T10:17:10.123Z" +}] as TestrunStatus[]; + +describe('HistoryComponent', () => { + let component: HistoryComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + let mockService: SpyObj; + + beforeEach(() => { + mockService = jasmine.createSpyObj(['fetchHistory', 'getHistory', 'getResultClass']); + TestBed.configureTestingModule({ + imports: [HistoryModule, BrowserAnimationsModule], + providers: [{provide: TestRunService, useValue: mockService}], + declarations: [HistoryComponent] + }); + fixture = TestBed.createComponent(HistoryComponent); + component = fixture.componentInstance; + compiled = fixture.nativeElement as HTMLElement; + fixture.detectChanges(); + }); + + describe('Class tests', () => { + beforeEach(() => { + mockService.getHistory.and.returnValue(of(history)); + }) + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should set history value', () => { + component.ngOnInit(); + + component.history$.subscribe(res => { + expect(res).toEqual(history) + }) + }); + + it('#getFormattedDateString should return string in the format "d MMM y H:mm"', () => { + const expectedResult = '23 Jun 2023 10:11'; + + const result = component.getFormattedDateString(history[0].started); + + expect(result).toEqual(expectedResult); + }); + + it('#getFormattedDateString should return empty string if no date', () => { + const expectedResult = ''; + + const result = component.getFormattedDateString(null); + + expect(result).toEqual(expectedResult); + }); + + it('#getDuration should return dates duration in minutes and seconds', () => { + const expectedResult = '06m 10s'; + + const result = component.getDuration(history[0].started, history[0].finished); + + expect(result).toEqual(expectedResult); + }); + + it('#getDuration should return empty string if any of dates are not provided', () => { + const expectedResult = ''; + + const result = component.getDuration(history[0].started, null); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('DOM tests', () => { + describe('with no devices', () => { + beforeEach(() => { + mockService.getHistory.and.returnValue(of([])); + fixture.detectChanges(); + }) + + it('should have empty message', () => { + const empty = compiled.querySelector('.results-content-empty'); + expect(empty).toBeTruthy(); + }); + }); + + describe('with devices', () => { + beforeEach(() => { + mockService.getHistory.and.returnValue(of(history)); + mockService.getResultClass.and.returnValue( + { green: false, red: true, blue: false, grey: false } + ); + component.ngOnInit(); + fixture.detectChanges(); + }) + + it('should have data table', () => { + const table = compiled.querySelector('table'); + + expect(table).toBeTruthy(); + }); + + it('should have addition valid class on table cell "Status"', () => { + const statusResultEl = compiled.querySelector('.table-cell-result-text'); + + expect(statusResultEl?.classList).toContain('red'); + }); + + it('should have report link', () => { + const link = compiled.querySelector('.download-report-link') as HTMLAnchorElement; + + expect(link.href).toEqual('https://api.testrun.io/report.pdf'); + expect(link.download).toEqual('delta_03-din-src_1.2.2_compliant_23_jun_2023_10:11'); + expect(link.title).toEqual('Download report for Test Run # Delta 03-DIN-SRC 1.2.2 23 Jun 2023 10:11'); + }); + }); + }); +}); diff --git a/modules/ui/src/app/history/history.component.ts b/modules/ui/src/app/history/history.component.ts new file mode 100644 index 000000000..9f6050e94 --- /dev/null +++ b/modules/ui/src/app/history/history.component.ts @@ -0,0 +1,64 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Component, OnInit} from '@angular/core'; +import {TestRunService} from '../test-run.service'; +import {Observable} from 'rxjs/internal/Observable'; +import {StatusResultClassName, TestrunStatus} from '../model/testrun-status'; +import {DatePipe} from '@angular/common'; + +@Component({ + selector: 'app-history', + templateUrl: './history.component.html', + styleUrls: ['./history.component.scss'] +}) +export class HistoryComponent implements OnInit { + history$!: Observable; + displayedColumns: string[] = ['started', 'duration', 'device', 'firmware', 'result', 'report']; + + constructor(private testRunService: TestRunService, private datePipe: DatePipe) { + this.testRunService.fetchHistory(); + } + + ngOnInit() { + this.history$ = this.testRunService.getHistory(); + } + + getFormattedDateString(date: string | null) { + return date ? this.datePipe.transform(date, 'd MMM y H:mm') : ''; + } + + private transformDate(date: number, format: string) { + return this.datePipe.transform(date, format); + } + + public getDuration(started: string | null, finished: string | null): string { + if (!started || !finished) { + return ''; + } + const startedDate = new Date(started); + const finishedDate = new Date(finished); + + const durationMillisecond = finishedDate.getTime() - startedDate.getTime(); + const durationMinuts = this.transformDate(durationMillisecond, 'mm'); + const durationSeconds = this.transformDate(durationMillisecond, 'ss'); + + return `${durationMinuts}m ${durationSeconds}s` + } + + public getResultClass(status: string): StatusResultClassName { + return this.testRunService.getResultClass(status); + } +} diff --git a/modules/ui/src/app/history/history.module.ts b/modules/ui/src/app/history/history.module.ts new file mode 100644 index 000000000..458e617ac --- /dev/null +++ b/modules/ui/src/app/history/history.module.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {NgModule} from '@angular/core'; +import {CommonModule, DatePipe} from '@angular/common'; +import {HistoryComponent} from './history.component'; +import {HistoryRoutingModule} from './history-routing.module'; +import {MatTableModule} from '@angular/material/table'; +import {MatIconModule} from '@angular/material/icon'; +import {MatToolbarModule} from '@angular/material/toolbar'; +import {DownloadReportComponent} from '../components/download-report/download-report.component'; + +@NgModule({ + declarations: [ + HistoryComponent, + ], + imports: [ + CommonModule, + HistoryRoutingModule, + MatTableModule, + MatIconModule, + MatToolbarModule, + DownloadReportComponent + ], + providers: [DatePipe] +}) +export class HistoryModule { +} diff --git a/modules/ui/src/app/mocks/device.mock.ts b/modules/ui/src/app/mocks/device.mock.ts new file mode 100644 index 000000000..3bb32e683 --- /dev/null +++ b/modules/ui/src/app/mocks/device.mock.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Device} from '../model/device'; + +export const device = { + "manufacturer": "Delta", + "model": "O3-DIN-CPU", + "mac_addr": "00:1e:42:35:73:c4", + "test_modules": { + "dns": { + "enabled": true, + } + } +} as Device; diff --git a/modules/ui/src/app/mocks/progress.mock.ts b/modules/ui/src/app/mocks/progress.mock.ts new file mode 100644 index 000000000..5a9678851 --- /dev/null +++ b/modules/ui/src/app/mocks/progress.mock.ts @@ -0,0 +1,69 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {IResult, StatusOfTestrun, TestrunStatus, TestsData} from '../model/testrun-status'; + +const TEST_DATA_RESULT: IResult[] = [ + { + name: 'dns.network.hostname_resolution', + description: 'The device should resolve hostnames', + result: 'Compliant' + }, + { + name: 'dns.network.from_dhcp', + description: 'The device should use the DNS server provided by the DHCP server', + result: 'Non-Compliant' + } +] + +export const TEST_DATA: TestsData = { + total: 26, + results: TEST_DATA_RESULT +} + +const PROGRESS_DATA_RESPONSE = ((status: string, finished: string | null, tests: TestsData | IResult[], report?: string) => { + return { + status, + device: { + manufacturer: 'Delta', + model: '03-DIN-CPU', + mac_addr: '01:02:03:04:05:06', + firmware: '1.2.2' + }, + started: '2023-06-22T09:20:00.123Z', + finished, + tests, + report + } +}); + +export const MOCK_PROGRESS_DATA_IN_PROGRESS: TestrunStatus = PROGRESS_DATA_RESPONSE(StatusOfTestrun.InProgress, null, TEST_DATA); +export const MOCK_PROGRESS_DATA_COMPLIANT: TestrunStatus = PROGRESS_DATA_RESPONSE( + StatusOfTestrun.Compliant, '2023-06-22T09:20:00.123Z', TEST_DATA_RESULT, 'https://api.testrun.io/report.pdf' +); + +export const MOCK_PROGRESS_DATA_CANCELLED: TestrunStatus = PROGRESS_DATA_RESPONSE(StatusOfTestrun.Cancelled, null, TEST_DATA); + +export const MOCK_PROGRESS_DATA_NOT_STARTED: TestrunStatus = { + ...MOCK_PROGRESS_DATA_IN_PROGRESS, + status: StatusOfTestrun.Idle, + started: null +}; + +export const MOCK_PROGRESS_DATA_WAITING_FOR_DEVICE: TestrunStatus = { + ...MOCK_PROGRESS_DATA_IN_PROGRESS, + status: StatusOfTestrun.WaitingForDevice, + started: null +}; diff --git a/modules/ui/src/app/model/device.ts b/modules/ui/src/app/model/device.ts new file mode 100644 index 000000000..2f5314037 --- /dev/null +++ b/modules/ui/src/app/model/device.ts @@ -0,0 +1,40 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export interface Device { + manufacturer: string; + model: string; + mac_addr: string; + test_modules?: TestModules; + firmware?: string; +} + +/** + * Test Modules interface used to send on backend + */ +export interface TestModules { + [key: string]: { + enabled: boolean; + } +} + +/** + * Test Module interface used on ui + */ +export interface TestModule { + displayName: string, + name: string, + enabled: boolean, +} diff --git a/modules/ui/src/app/model/setting.ts b/modules/ui/src/app/model/setting.ts new file mode 100644 index 000000000..843efad0d --- /dev/null +++ b/modules/ui/src/app/model/setting.ts @@ -0,0 +1,21 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export interface SystemConfig { + network: { + device_intf?: string; + internet_intf?: string; + } +} diff --git a/modules/ui/src/app/model/testrun-status.ts b/modules/ui/src/app/model/testrun-status.ts new file mode 100644 index 000000000..592255f79 --- /dev/null +++ b/modules/ui/src/app/model/testrun-status.ts @@ -0,0 +1,75 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Device} from './device'; + +export interface TestrunStatus { + status: string; + device: IDevice; + started: string | null; + finished: string | null; + tests?: TestsResponse; + report?: string; +} + +export interface TestsData { + total?: number; + results?: IResult[]; +} + +type TestsResponse = TestsData | IResult[]; + +export interface IDevice extends Device { + firmware: string; +} + +export interface IResult { + name: string; + description: string; + result: string; +} + +export enum StatusOfTestrun { + InProgress = 'In Progress', + WaitingForDevice = 'Waiting for Device', + Cancelled = 'Cancelled', + Failed = 'Failed', + Compliant = 'Compliant', // used for Completed + NonCompliant = 'Non-Compliant', // used for Completed + SmartReady = 'Smart Ready', // used for Completed + Idle = 'Idle' +} + +export enum StatusOfTestResult { + Compliant = 'Compliant', // device supports feature + SmartReady = 'Smart Ready', + NonCompliant = 'Non-Compliant', // device does not support feature but feature is required + Skipped = 'Skipped', + NotStarted = 'Not Started', + Error = 'Error', // test failed to run + Info = 'Informational' // nice to know information, not necessarily compliant/non-compliant +} + +export interface StatusResultClassName { + green: boolean, + red: boolean, + blue: boolean, + grey: boolean +} + +export type TestrunStatusKey = keyof typeof StatusOfTestrun; +export type TestrunStatusValue = typeof StatusOfTestrun[TestrunStatusKey]; +export type TestResultKey = keyof typeof StatusOfTestResult; +export type TestResultValue = typeof StatusOfTestResult[TestResultKey]; diff --git a/modules/ui/src/app/notification.service.spec.ts b/modules/ui/src/app/notification.service.spec.ts new file mode 100644 index 000000000..b10bf36a8 --- /dev/null +++ b/modules/ui/src/app/notification.service.spec.ts @@ -0,0 +1,61 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {TestBed} from '@angular/core/testing'; + +import {NotificationService} from './notification.service'; +import {MatSnackBar} from '@angular/material/snack-bar'; + +describe('NotificationService', () => { + let service: NotificationService; + + const mockMatSnackBar = { + open: () => { + } + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + {provide: MatSnackBar, useValue: mockMatSnackBar}, + ] + }); + service = TestBed.inject(NotificationService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('notify', () => { + it('should open snackbar with message', () => { + const matSnackBarSpy = spyOn(mockMatSnackBar, 'open').and.stub(); + + service.notify('something good happened'); + + expect(matSnackBarSpy).toHaveBeenCalled(); + + const args = matSnackBarSpy.calls.argsFor(0); + expect(args.length).toBe(3); + expect(args[0]).toBe('something good happened'); + expect(args[1]).toBe('x'); + expect(args[2]).toEqual({ + horizontalPosition: 'right', + panelClass: 'test-run-notification', + }); + }); + }); + +}); diff --git a/modules/ui/src/app/notification.service.ts b/modules/ui/src/app/notification.service.ts new file mode 100644 index 000000000..1e1f4a43c --- /dev/null +++ b/modules/ui/src/app/notification.service.ts @@ -0,0 +1,32 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Injectable} from '@angular/core'; +import {MatSnackBar} from '@angular/material/snack-bar'; + +@Injectable({ + providedIn: 'root' +}) +export class NotificationService { + constructor(private snackBar: MatSnackBar) { + } + + notify(message: string) { + this.snackBar.open(message, 'x', { + horizontalPosition: 'right', + panelClass: 'test-run-notification' + }) + } +} diff --git a/modules/ui/src/app/progress/progress-breadcrumbs/progress-breadcrumbs.component.html b/modules/ui/src/app/progress/progress-breadcrumbs/progress-breadcrumbs.component.html new file mode 100644 index 000000000..d51097652 --- /dev/null +++ b/modules/ui/src/app/progress/progress-breadcrumbs/progress-breadcrumbs.component.html @@ -0,0 +1,27 @@ + +
    + + +
+ diff --git a/modules/ui/src/app/progress/progress-breadcrumbs/progress-breadcrumbs.component.scss b/modules/ui/src/app/progress/progress-breadcrumbs/progress-breadcrumbs.component.scss new file mode 100644 index 000000000..80f4d45e3 --- /dev/null +++ b/modules/ui/src/app/progress/progress-breadcrumbs/progress-breadcrumbs.component.scss @@ -0,0 +1,67 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '@angular/material' as mat; +@import "../../../theming/colors"; + +:host { + width: 100%; + overflow: hidden; +} + +ul { + display: flex; + list-style: none; + margin: 0; + padding: 0 8px; + + .breadcrumb-item { + display: flex; + flex: 0 1 auto; + max-width: 25%; + align-items: center; + justify-content: center; + gap: 8px; + margin-right: 16px; + + &.first { + margin-right: 6px; + flex-shrink: 0; + } + } + + .icon-home { + color: $dark-grey; + flex-shrink: 0; + } + + .icon { + color: $secondary; + flex-shrink: 0; + } + + .breadcrumb-text { + color: mat.get-color-from-palette($color-primary, 600); + text-align: center; + /* font-family: SF Pro Text; */ + font-size: 12px; + font-weight: 700; + line-height: 16px; + letter-spacing: 0.3px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} diff --git a/modules/ui/src/app/progress/progress-breadcrumbs/progress-breadcrumbs.component.spec.ts b/modules/ui/src/app/progress/progress-breadcrumbs/progress-breadcrumbs.component.spec.ts new file mode 100644 index 000000000..18ea87fa5 --- /dev/null +++ b/modules/ui/src/app/progress/progress-breadcrumbs/progress-breadcrumbs.component.spec.ts @@ -0,0 +1,38 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ProgressBreadcrumbsComponent} from './progress-breadcrumbs.component'; +import {MatIconModule} from '@angular/material/icon'; + +describe('ProgressBreadcrumbsComponent', () => { + let component: ProgressBreadcrumbsComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ProgressBreadcrumbsComponent], + imports: [MatIconModule] + }); + fixture = TestBed.createComponent(ProgressBreadcrumbsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/modules/ui/src/app/progress/progress-breadcrumbs/progress-breadcrumbs.component.ts b/modules/ui/src/app/progress/progress-breadcrumbs/progress-breadcrumbs.component.ts new file mode 100644 index 000000000..34ffc18b3 --- /dev/null +++ b/modules/ui/src/app/progress/progress-breadcrumbs/progress-breadcrumbs.component.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {Observable} from 'rxjs/internal/Observable'; + +@Component({ + selector: 'app-progress-breadcrumbs', + templateUrl: './progress-breadcrumbs.component.html', + styleUrls: ['./progress-breadcrumbs.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ProgressBreadcrumbsComponent { + @Input() breadcrumbs$!: Observable; +} diff --git a/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.html b/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.html new file mode 100644 index 000000000..fefc48010 --- /dev/null +++ b/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.html @@ -0,0 +1,79 @@ + +
+ Start New Testrun + +
+ + + + + + + + Firmware + + Please enter device firmware + + Firmware is required + + + + + + +
+ + + + + + +
+ + + + + + Firmware + + Please enter device firmware + + Firmware is required + + + + diff --git a/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.scss b/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.scss new file mode 100644 index 000000000..a92c2671d --- /dev/null +++ b/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.scss @@ -0,0 +1,65 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../theming/colors"; +@import "../../../theming/variables"; + +:host { + display: grid; + grid-template-rows: 1fr; + overflow: hidden; + width: 450px; +} + +.progress-initiate-form { + display: grid; + overflow: auto; + max-height: 100vh; +} + +.progress-initiate-form-title { + color: $grey-800; + font-size: 22px; + line-height: 28px; + padding: 24px; + border-bottom: 1px solid $light-grey; +} + +.progress-initiate-form-content { + overflow: auto; + min-height: 78px; + padding: 32px 0; + display: grid; + gap: 32px; + justify-content: center; + justify-items: center; + grid-template-columns: 1fr; + + & > * { + width: $device-item-width; + box-sizing: border-box; + } +} + +.progress-initiate-form-actions { + min-height: 30px; + justify-content: space-between; + padding: 16px; + border-top: 1px solid $lighter-grey; +} + +.hidden { + display: none; +} diff --git a/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.spec.ts b/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.spec.ts new file mode 100644 index 000000000..5e4bf3c97 --- /dev/null +++ b/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.spec.ts @@ -0,0 +1,300 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ComponentFixture, discardPeriodicTasks, fakeAsync, TestBed, tick} from '@angular/core/testing'; + +import {ProgressInitiateFormComponent} from './progress-initiate-form.component'; +import {MatDialogModule, MatDialogRef} from '@angular/material/dialog'; +import {TestRunService} from '../../test-run.service'; +import {BehaviorSubject} from 'rxjs/internal/BehaviorSubject'; +import {Device} from '../../model/device'; +import {DeviceItemComponent} from '../../components/device-item/device-item.component'; +import {ReactiveFormsModule} from '@angular/forms'; +import {MatInputModule} from '@angular/material/input'; +import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; +import {DeviceTestsComponent} from '../../components/device-tests/device-tests.component'; +import {device} from '../../mocks/device.mock'; +import {of} from 'rxjs'; +import {MOCK_PROGRESS_DATA_IN_PROGRESS, MOCK_PROGRESS_DATA_WAITING_FOR_DEVICE} from '../../mocks/progress.mock'; +import {throwError} from 'rxjs/internal/observable/throwError'; +import {NotificationService} from '../../notification.service'; + +describe('ProgressInitiateFormComponent', () => { + let component: ProgressInitiateFormComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + let testRunServiceMock: jasmine.SpyObj; + let notificationServiceMock: jasmine.SpyObj; + + notificationServiceMock = jasmine.createSpyObj(['notify']); + testRunServiceMock = jasmine.createSpyObj(['getDevices', 'fetchDevices', 'getTestModules', 'startTestrun', 'systemStatus$', 'getSystemStatus']); + testRunServiceMock.getTestModules.and.returnValue([ + { + displayName: "Connection", + name: "connection", + enabled: true + }, + { + displayName: "DNS", + name: "dns", + enabled: false + }, + ]); + testRunServiceMock.getDevices.and.returnValue(new BehaviorSubject([device, device])); + testRunServiceMock.startTestrun.and.returnValue(of(true)); + testRunServiceMock.systemStatus$ = of(MOCK_PROGRESS_DATA_WAITING_FOR_DEVICE); + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ProgressInitiateFormComponent], + providers: [ + {provide: TestRunService, useValue: testRunServiceMock}, + {provide: NotificationService, useValue: notificationServiceMock}, + { + provide: MatDialogRef, + useValue: { + close: () => { + } + } + }], + imports: [ + MatDialogModule, + DeviceItemComponent, + ReactiveFormsModule, + MatInputModule, + BrowserAnimationsModule, + DeviceTestsComponent, + ] + }); + fixture = TestBed.createComponent(ProgressInitiateFormComponent); + component = fixture.componentInstance; + compiled = fixture.nativeElement as HTMLElement; + }); + + afterEach(() => { + testRunServiceMock.getSystemStatus.calls.reset(); + notificationServiceMock.notify.calls.reset(); + }); + + describe('when test run started', () => { + beforeEach(() => { + component.testRunStarted = true; + }); + describe('with status "Waiting for device"', () => { + beforeEach(async () => { + testRunServiceMock.systemStatus$ = of(MOCK_PROGRESS_DATA_WAITING_FOR_DEVICE); + }); + + it('should call again getSystemStatus', fakeAsync(() => { + fixture.detectChanges(); + tick(10000); + + expect(testRunServiceMock.getSystemStatus).toHaveBeenCalledTimes(2); + + discardPeriodicTasks(); + })); + + it('should notify about status', fakeAsync(() => { + fixture.detectChanges(); + + expect(notificationServiceMock.notify).toHaveBeenCalledWith('Waiting for Device'); + + discardPeriodicTasks(); + })); + + }); + + describe('with status not "Waiting for device"', () => { + beforeEach(async () => { + testRunServiceMock.systemStatus$ = of(MOCK_PROGRESS_DATA_IN_PROGRESS); + }); + + it('should call again getSystemStatus', fakeAsync(() => { + spyOn(component.dialogRef, 'close'); + fixture.detectChanges(); + + expect(component.dialogRef.close).toHaveBeenCalled(); + })); + }) + }); + + describe('Class tests', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should close dialog', () => { + spyOn(component.dialogRef, 'close'); + component.cancel(); + expect(component.dialogRef.close).toHaveBeenCalled(); + }); + + it('should set devices$ value', () => { + component.ngOnInit(); + + component.devices$.subscribe(res => { + expect(res).toEqual([device, device]) + }) + }); + + it('should update selectedDevice on deviceSelected', () => { + const newDevice = Object.assign({}, device, {manufacturer: 'Gamma'}) + component.deviceSelected(newDevice); + + expect(component.selectedDevice).toEqual(newDevice); + }); + + it('should reset selectedDevice and firmware on changeDevice', () => { + component.changeDevice(); + + expect(component.selectedDevice).toEqual(null); + expect(component.firmware.value).toEqual(''); + }); + + describe('#startNewTestRun', () => { + it('should add required error if firmware is empty', () => { + component.firmware.setValue(''); + component.startTestRun(); + + expect(component.firmware.errors).toBeTruthy(); + expect(component.firmware.errors ? component.firmware.errors['required'] : false).toEqual(true); + }); + + describe('when selectedDevice is present and firmware is filled', () => { + beforeEach(() => { + component.firmware.setValue('firmware'); + component.selectedDevice = device; + }); + + it('should call startTestRun with device', () => { + component.startTestRun(); + + expect(testRunServiceMock.startTestrun).toHaveBeenCalledWith({ + "manufacturer": "Delta", + "model": "O3-DIN-CPU", + "mac_addr": "00:1e:42:35:73:c4", + "firmware": "firmware", + "test_modules": { + "dns": { + "enabled": true, + } + } + }); + }); + + describe('when result is success', () => { + it('should call getSystemStatus', () => { + testRunServiceMock.startTestrun.and.returnValue(of(true)); + component.startTestRun(); + + expect(testRunServiceMock.getSystemStatus).toHaveBeenCalled(); + }); + }) + + describe('when error happened', () => { + it('should notify about error', () => { + testRunServiceMock.startTestrun.and.returnValue(throwError('error')); + + component.startTestRun(); + + expect(component.startInterval).toEqual(false); + expect(notificationServiceMock.notify).toHaveBeenCalledWith('error'); + }); + }) + + }) + }); + }); + + describe('DOM tests', () => { + describe('empty device', () => { + beforeEach(() => { + component.selectedDevice = null; + fixture.detectChanges(); + }); + + it('should have device list', () => { + const deviceList = compiled.querySelectorAll('app-device-item'); + + expect(deviceList.length).toEqual(2); + }); + + it('should select device on device click', () => { + spyOn(component, 'deviceSelected'); + const deviceList = compiled.querySelector('app-device-item button') as HTMLButtonElement; + deviceList.click(); + + expect(component.deviceSelected).toHaveBeenCalled(); + }); + + it('should disable change device and start buttons', () => { + const changeDevice = compiled.querySelector('.progress-initiate-form-actions-change-device') as HTMLButtonElement; + const start = compiled.querySelector('.progress-initiate-form-actions-start') as HTMLButtonElement; + + expect(changeDevice.disabled).toEqual(true); + expect(start.disabled).toEqual(true); + }); + }) + + describe('with device', () => { + beforeEach(() => { + component.selectedDevice = device; + fixture.detectChanges(); + }); + + it('should display selected device if device selected', () => { + const deviceItem = compiled.querySelector('app-device-item'); + + expect(deviceItem).toBeTruthy(); + }); + + it('should display firmware if device selected', () => { + const firmware = compiled.querySelector('input'); + + expect(firmware).toBeTruthy(); + }); + + it('should display tests if device selected', () => { + const testsForm = compiled.querySelector('app-device-tests form'); + const tests = compiled.querySelectorAll('app-device-tests mat-checkbox'); + + expect(testsForm).toBeTruthy(); + expect(testsForm?.classList.contains('disabled')).toEqual(true); + expect(tests.length).toEqual(2); + }); + + it('should change device on change device button click', () => { + spyOn(component, 'changeDevice'); + const button = compiled.querySelector('.progress-initiate-form-actions-change-device') as HTMLButtonElement; + button.click(); + + expect(component.changeDevice).toHaveBeenCalled(); + }); + + it('should start test run on start button click', () => { + spyOn(component, 'startTestRun'); + const button = compiled.querySelector('.progress-initiate-form-actions-start') as HTMLButtonElement; + button.click(); + + expect(component.startTestRun).toHaveBeenCalled(); + }); + + }); + }); +}); diff --git a/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.ts b/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.ts new file mode 100644 index 000000000..2e4c9ee4e --- /dev/null +++ b/modules/ui/src/app/progress/progress-initiate-form/progress-initiate-form.component.ts @@ -0,0 +1,143 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {Component, OnDestroy, OnInit} from '@angular/core'; +import {MatDialogRef} from '@angular/material/dialog'; +import {TestRunService} from '../../test-run.service'; +import {Observable} from 'rxjs/internal/Observable'; +import {Device, TestModule} from '../../model/device'; +import {FormArray, FormBuilder, FormGroup} from '@angular/forms'; +import {DeviceValidators} from '../../device-repository/device-form/device.validators'; +import {StatusOfTestrun, TestrunStatus} from '../../model/testrun-status'; +import {tap} from 'rxjs/internal/operators/tap'; +import {interval} from 'rxjs/internal/observable/interval'; +import {takeUntil} from 'rxjs/internal/operators/takeUntil'; +import {Subject} from 'rxjs/internal/Subject'; +import {NotificationService} from '../../notification.service'; + +@Component({ + selector: 'app-progress-initiate-form', + templateUrl: './progress-initiate-form.component.html', + styleUrls: ['./progress-initiate-form.component.scss'] +}) +export class ProgressInitiateFormComponent implements OnInit, OnDestroy { + initiateForm!: FormGroup; + devices$!: Observable; + selectedDevice: Device | null = null; + testModules: TestModule[] = []; + public systemStatus$!: Observable; + startInterval = false; + destroy$: Subject = new Subject(); + + constructor( + public dialogRef: MatDialogRef, + private readonly testRunService: TestRunService, + private fb: FormBuilder, + private deviceValidators: DeviceValidators, + private notificationService: NotificationService) { + } + + get firmware() { + return this.initiateForm.get('firmware')!; + } + + cancel(): void { + this.dialogRef.close(); + } + + testRunStarted = false; + + ngOnInit() { + this.devices$ = this.testRunService.getDevices(); + this.createInitiateForm(); + this.testModules = this.testRunService.getTestModules(); + + this.testRunService.systemStatus$.pipe( + tap((res) => { + if (this.testRunStarted) { + if (res.status === StatusOfTestrun.WaitingForDevice && !this.startInterval) { + this.pullingSystemStatusData(); + this.notify(res.status); + } + if (res.status !== StatusOfTestrun.WaitingForDevice) { + this.destroy$.next(true); + this.startInterval = false; + this.dialogRef.close(); + } + } + }), + takeUntil(this.destroy$), + ).subscribe(); + } + + deviceSelected(device: Device) { + this.selectedDevice = device; + } + + changeDevice() { + this.selectedDevice = null; + this.firmware.setValue(''); + } + + startTestRun() { + if (!this.firmware.value.trim()) { + this.firmware.setErrors({required: true}); + } + + if (this.initiateForm.invalid) { + this.initiateForm.markAllAsTouched(); + return; + } + + if (this.selectedDevice) { + this.selectedDevice.firmware = this.firmware.value.trim(); + this.testRunService.startTestrun(this.selectedDevice) + .pipe(takeUntil(this.destroy$)) + .subscribe( + () => { + this.testRunStarted = true; + this.testRunService.getSystemStatus(); + }, + (error: string) => { + this.startInterval = false; + this.notify(error); + }); + } + } + + private createInitiateForm() { + this.initiateForm = this.fb.group({ + firmware: ['', [this.deviceValidators.deviceStringFormat()]], + test_modules: new FormArray([]) + }); + } + + ngOnDestroy() { + this.destroy$.next(true); + this.destroy$.unsubscribe(); + } + + private pullingSystemStatusData(): void { + this.startInterval = true; + interval(5000).pipe( + takeUntil(this.destroy$), + tap(() => this.testRunService.getSystemStatus()), + ).subscribe(); + } + + private notify(message: string) { + this.notificationService.notify(message); + } +} diff --git a/modules/ui/src/app/progress/progress-routing.module.ts b/modules/ui/src/app/progress/progress-routing.module.ts new file mode 100644 index 000000000..841792955 --- /dev/null +++ b/modules/ui/src/app/progress/progress-routing.module.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {NgModule} from '@angular/core'; +import {RouterModule, Routes} from '@angular/router'; +import {ProgressComponent} from './progress.component'; + +const routes: Routes = [{path: '', component: ProgressComponent}]; + +@NgModule({ + imports: [RouterModule.forChild(routes)], + exports: [RouterModule] +}) +export class ProgressRoutingModule { +} diff --git a/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.html b/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.html new file mode 100644 index 000000000..c8e4430db --- /dev/null +++ b/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.html @@ -0,0 +1,45 @@ + + +
+
+

+ Test status +

+

+ {{getTestStatus(data)}} + {{getTestsResult(data)}} +

+
+
+ + +
+

+ Test result +

+

+ {{data.status}} +

+
+
+
+
diff --git a/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.scss b/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.scss new file mode 100644 index 000000000..b10d8b893 --- /dev/null +++ b/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.scss @@ -0,0 +1,72 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '@angular/material' as mat; +@import "../../../theming/colors"; + +:host { + height: 152px; +} + +.progress-card { + display: flex; + flex-direction: column; + justify-content: space-between; + width: 295px; + height: 100%; + box-sizing: border-box; + padding: 16px 32px; + + &.progress { + background-color: mat.get-color-from-palette($color-primary, 700); + } + + &.completed-success { + background-color: mat.get-color-from-palette($color-accent, 700); + } + + &.completed-failed { + background-color: $red-800; + } + + &.canceled { + background-color: $secondary; + } + + p { + margin: 0; + } + + .progress-card-status-title, + .progress-card-result-title { + color: $white; + font-size: 14px; + line-height: 20px; + } + + .progress-card-status-text, + .progress-card-result-text { + display: flex; + justify-content: space-between; + color: $white; + font-size: 24px; + font-weight: 400; + line-height: 32px; + } + + .progress-bar { + padding-bottom: 28px; + } +} diff --git a/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.spec.ts b/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.spec.ts new file mode 100644 index 000000000..c56957ad2 --- /dev/null +++ b/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.spec.ts @@ -0,0 +1,274 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ProgressStatusCardComponent} from './progress-status-card.component'; +import {StatusOfTestrun, TestrunStatus} from '../../model/testrun-status'; +import {MOCK_PROGRESS_DATA_CANCELLED, MOCK_PROGRESS_DATA_COMPLIANT, MOCK_PROGRESS_DATA_IN_PROGRESS} from '../../mocks/progress.mock'; +import {ProgressModule} from '../progress.module'; +import {of} from 'rxjs'; + +describe('ProgressStatusCardComponent', () => { + let component: ProgressStatusCardComponent; + let fixture: ComponentFixture; + + describe('Class tests', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ProgressStatusCardComponent] + }); + fixture = TestBed.createComponent(ProgressStatusCardComponent); + component = fixture.componentInstance; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('#getClass', () => { + it('should have class "progress" if status "In Progress"', () => { + const expectedResult = { + progress: true, + 'completed-success': false, + 'completed-failed': false, + canceled: false + }; + + const result = component.getClass(StatusOfTestrun.InProgress); + + expect(result).toEqual(expectedResult); + }); + + it('should have class "completed-success" if status "Compliant"', () => { + const expectedResult = { + progress: false, + 'completed-success': true, + 'completed-failed': false, + canceled: false + }; + + const result = component.getClass(StatusOfTestrun.Compliant); + + expect(result).toEqual(expectedResult); + }); + + it('should have class "completed-failed" if status "Non Compliant"', () => { + const expectedResult = { + progress: false, + 'completed-success': false, + 'completed-failed': true, + canceled: false + }; + + const result = component.getClass(StatusOfTestrun.NonCompliant); + + expect(result).toEqual(expectedResult); + }); + + it('should have class "canceled" if status "Cancelled"', () => { + const expectedResult = { + progress: false, + 'completed-success': false, + 'completed-failed': false, + canceled: true + }; + + const result = component.getClass(StatusOfTestrun.Cancelled); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('#getTestsResult', () => { + it('should return correct test result if status "In Progress"', () => { + const expectedResult = '2/26'; + + const result = component.getTestsResult(MOCK_PROGRESS_DATA_IN_PROGRESS); + + expect(result).toEqual(expectedResult); + }); + + it('should return correct test result if status "Compliant"', () => { + const expectedResult = '2/2'; + + const result = component.getTestsResult(MOCK_PROGRESS_DATA_COMPLIANT); + + expect(result).toEqual(expectedResult); + }); + + it('should return correct test result if status "Cancelled"', () => { + const expectedResult = '2/26'; + + const result = component.getTestsResult(MOCK_PROGRESS_DATA_CANCELLED); + + expect(result).toEqual(expectedResult); + }); + + it('should return empty string if no data', () => { + const expectedResult = ''; + + const result = component.getTestsResult({} as TestrunStatus); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('#getTestStatus', () => { + it('should return test status "Complete" if testrun is finished', () => { + const expectedResult = 'Complete'; + + const result = component.getTestStatus(MOCK_PROGRESS_DATA_COMPLIANT); + + expect(result).toEqual(expectedResult); + }); + + it('should return test status "Incomplete" if status "Cancelled"', () => { + const expectedResult = 'Incomplete'; + + const result = component.getTestStatus(MOCK_PROGRESS_DATA_CANCELLED); + + expect(result).toEqual(expectedResult); + }); + + it('should return test status "In Progress" if status "In Progress"', () => { + const expectedResult = 'In Progress'; + + const result = component.getTestStatus(MOCK_PROGRESS_DATA_IN_PROGRESS); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('#getProgressValue', () => { + it('should return correct progress value if status "In Progress"', () => { + const expectedResult = Math.round(2 / 26 * 100); + + const result = component.getProgressValue(MOCK_PROGRESS_DATA_IN_PROGRESS); + + expect(result).toEqual(expectedResult); + }); + + it('should return zero if no data', () => { + const expectedResult = 0; + + const result = component.getProgressValue({} as TestrunStatus); + + expect(result).toEqual(expectedResult); + }); + }); + }); + + describe('DOM tests', () => { + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ProgressStatusCardComponent], + imports: [ProgressModule] + }).compileComponents(); + + fixture = TestBed.createComponent(ProgressStatusCardComponent); + compiled = fixture.nativeElement as HTMLElement; + component = fixture.componentInstance; + }); + + describe('with not systemStatus$ data', () => { + beforeEach(() => { + (component.systemStatus$ as any) = of(null); + fixture.detectChanges(); + }); + + it('should not have content', () => { + const progressCardEl = compiled.querySelector('.progress-card'); + + expect(progressCardEl).toBeNull(); + }); + }); + + describe('with available systemStatus$ data, as Cancelled', () => { + beforeEach(() => { + component.systemStatus$ = of(MOCK_PROGRESS_DATA_CANCELLED); + fixture.detectChanges(); + }); + + it('should have progress card content', () => { + const progressCardEl = compiled.querySelector('.progress-card'); + + expect(progressCardEl).not.toBeNull(); + }); + + it('should have class "canceled" on progress card element', () => { + const progressCardEl = compiled.querySelector('.progress-card'); + + expect(progressCardEl?.classList).toContain('canceled'); + }); + + it('should not have progress bar element', () => { + const progressBarEl = compiled.querySelector('.progress-bar'); + + expect(progressBarEl).toBeNull(); + }); + + it('should have progress card result', () => { + const progressCardResultEl = compiled.querySelector('.progress-card-result-text span'); + + expect(progressCardResultEl).not.toBeNull(); + expect(progressCardResultEl?.textContent).toEqual('Cancelled'); + }); + + it('should have progress card status text as "Incomplete"', () => { + const progressCardStatusText = compiled.querySelector('.progress-card-status-text > span'); + + expect(progressCardStatusText).not.toBeNull(); + expect(progressCardStatusText?.textContent).toEqual('Incomplete'); + }); + }); + + describe('with available systemStatus$ data, as "In Progress"', () => { + beforeEach(() => { + component.systemStatus$ = of(MOCK_PROGRESS_DATA_IN_PROGRESS); + fixture.detectChanges(); + }); + + + it('should have class "progress" on progress card element', () => { + const progressCardEl = compiled.querySelector('.progress-card'); + + expect(progressCardEl?.classList).toContain('progress'); + }); + + it('should have progress bar element', () => { + const progressBarEl = compiled.querySelector('.progress-bar'); + + expect(progressBarEl).not.toBeNull(); + }); + + it('should not have progress card result', () => { + const progressCardResultEl = compiled.querySelector('.progress-card-result-text span'); + + expect(progressCardResultEl).toBeNull(); + }); + + it('should have progress card status text as "In Progress"', () => { + const progressCardStatusText = compiled.querySelector('.progress-card-status-text > span'); + + expect(progressCardStatusText).not.toBeNull(); + expect(progressCardStatusText?.textContent).toEqual('In Progress'); + }); + }); + }); + +}); diff --git a/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.ts b/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.ts new file mode 100644 index 000000000..417de536c --- /dev/null +++ b/modules/ui/src/app/progress/progress-status-card/progress-status-card.component.ts @@ -0,0 +1,69 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {Observable} from 'rxjs/internal/Observable'; +import {IResult, StatusOfTestrun, TestrunStatus, TestsData} from '../../model/testrun-status'; + +@Component({ + selector: 'app-progress-status-card', + templateUrl: './progress-status-card.component.html', + styleUrls: ['./progress-status-card.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ProgressStatusCardComponent { + @Input() systemStatus$!: Observable; + + public readonly StatusOfTestrun = StatusOfTestrun; + + public getClass(status: string): { progress: boolean, 'completed-success': boolean, 'completed-failed': boolean, canceled: boolean } { + return { + 'progress': status === StatusOfTestrun.InProgress, + 'completed-success': status === StatusOfTestrun.Compliant || status === StatusOfTestrun.SmartReady, + 'completed-failed': status === StatusOfTestrun.NonCompliant, + 'canceled': status === StatusOfTestrun.Cancelled + } + } + + public getTestsResult(data: TestrunStatus): string { + if (data.status === StatusOfTestrun.InProgress || data.status === StatusOfTestrun.Cancelled || data.finished) { + if ((data.tests as TestsData)?.results?.length && (data.tests as TestsData)?.total) { + return `${(data.tests as TestsData)?.results?.length}/${(data.tests as TestsData)?.total}` + } else if ((data.tests as IResult[])?.length) { + return `${(data.tests as IResult[])?.length}/${(data.tests as IResult[])?.length}` + } + } + return ''; + } + + public getTestStatus(data: TestrunStatus): string { + if (data.finished) { + return 'Complete'; + } else if (data.status === StatusOfTestrun.Cancelled) { + return 'Incomplete'; + } else { + return data.status; + } + } + + public getProgressValue(data: TestrunStatus): number { + const testData = data.tests as TestsData; + + if (testData && testData.total && testData.results?.length) { + return Math.round(testData.results.length / testData.total * 100); + } + return 0; + } +} diff --git a/modules/ui/src/app/progress/progress-table/progress-table.component.html b/modules/ui/src/app/progress/progress-table/progress-table.component.html new file mode 100644 index 000000000..ae6339733 --- /dev/null +++ b/modules/ui/src/app/progress/progress-table/progress-table.component.html @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + +
Name {{element.name}} Description {{element.description}} Result + + {{element.result}} + +
+
diff --git a/modules/ui/src/app/progress/progress-table/progress-table.component.scss b/modules/ui/src/app/progress/progress-table/progress-table.component.scss new file mode 100644 index 000000000..86a2da433 --- /dev/null +++ b/modules/ui/src/app/progress/progress-table/progress-table.component.scss @@ -0,0 +1,55 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@import "../../../theming/colors"; +@import "../../../theming/variables"; + +:host { + overflow-y: auto; + padding: 10px 0 12px; +} + +table, tr { + border: 1px solid $lighter-grey; + border-collapse: collapse; +} + +.progress-table { + .table-header-row { + height: 57px; + } + + .table-cell, + .table-header-cell { + font-family: $font-secondary; + font-size: 14px; + line-height: 20px; + } + + .table-header-cell { + font-weight: 500; + letter-spacing: 0.25px; + } + + .table-cell { + padding: 16px; + letter-spacing: 0.2px; + vertical-align: top; + } + + .table-cell-result { + min-width: 140px; + } +} diff --git a/modules/ui/src/app/progress/progress-table/progress-table.component.spec.ts b/modules/ui/src/app/progress/progress-table/progress-table.component.spec.ts new file mode 100644 index 000000000..c214b85ab --- /dev/null +++ b/modules/ui/src/app/progress/progress-table/progress-table.component.spec.ts @@ -0,0 +1,109 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ComponentFixture, TestBed} from '@angular/core/testing'; + +import {ProgressTableComponent} from './progress-table.component'; +import {IResult, StatusOfTestResult} from '../../model/testrun-status'; +import {MatTableModule} from '@angular/material/table'; +import {of} from 'rxjs'; +import {TEST_DATA} from '../../mocks/progress.mock'; +import {TestRunService} from '../../test-run.service'; + +describe('ProgressTableComponent', () => { + let component: ProgressTableComponent; + let fixture: ComponentFixture; + let testRunServiceMock: jasmine.SpyObj; + + testRunServiceMock = jasmine.createSpyObj(['getResultClass']); + + describe('Class tests', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ProgressTableComponent], + providers: [{provide: TestRunService, useValue: testRunServiceMock}], + }); + fixture = TestBed.createComponent(ProgressTableComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('#getResultClass should call the service method getResultClass"', () => { + const expectedResult = { + green: false, red: true, blue: false, grey: false + }; + + testRunServiceMock.getResultClass.and.returnValue(expectedResult); + + const result = component.getResultClass(StatusOfTestResult.NonCompliant); + + expect(testRunServiceMock.getResultClass).toHaveBeenCalledWith(StatusOfTestResult.NonCompliant); + expect(result).toEqual(expectedResult); + }); + }); + + describe('DOM tests', () => { + let compiled: HTMLElement; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [ProgressTableComponent], + providers: [{provide: TestRunService, useValue: testRunServiceMock}], + imports: [MatTableModule] + }).compileComponents(); + + fixture = TestBed.createComponent(ProgressTableComponent); + component = fixture.componentInstance; + compiled = fixture.nativeElement as HTMLElement; + }); + + describe('with not dataSource$ data', () => { + beforeEach(() => { + component.dataSource$ = of(undefined); + fixture.detectChanges(); + }); + + it('should be unavailable', () => { + const table = compiled.querySelector('.progress-table'); + + expect(table).toBeNull(); + }); + }); + + describe('with dataSource$ data', () => { + beforeEach(() => { + component.dataSource$ = of(TEST_DATA.results); + fixture.detectChanges(); + }); + + it('should be available', () => { + const table = compiled.querySelector('.progress-table'); + + expect(table).not.toBeNull(); + }); + + it('should have table rows as provided from data', () => { + const expectedRowsLength = (TEST_DATA.results as IResult[]).length; + const tableRows = compiled.querySelectorAll('.table-row'); + + expect(tableRows.length).toBe(expectedRowsLength); + }); + }); + }); +}); diff --git a/modules/ui/src/app/progress/progress-table/progress-table.component.ts b/modules/ui/src/app/progress/progress-table/progress-table.component.ts new file mode 100644 index 000000000..68d12433d --- /dev/null +++ b/modules/ui/src/app/progress/progress-table/progress-table.component.ts @@ -0,0 +1,37 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ChangeDetectionStrategy, Component, Input} from '@angular/core'; +import {Observable} from 'rxjs/internal/Observable'; +import {IResult, StatusResultClassName} from '../../model/testrun-status'; +import {TestRunService} from '../../test-run.service'; + +@Component({ + selector: 'app-progress-table', + templateUrl: './progress-table.component.html', + styleUrls: ['./progress-table.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ProgressTableComponent { + @Input() dataSource$!: Observable; + + displayedColumns: string[] = ['name', 'description', 'result']; + + constructor(private readonly testRunService: TestRunService) {} + + public getResultClass(result: string): StatusResultClassName { + return this.testRunService.getResultClass(result); + } +} diff --git a/modules/ui/src/app/progress/progress.component.html b/modules/ui/src/app/progress/progress.component.html new file mode 100644 index 000000000..d107374bd --- /dev/null +++ b/modules/ui/src/app/progress/progress.component.html @@ -0,0 +1,78 @@ + + + + +
+
+ + + + +
+
+ +

+ {{data.device.manufacturer}} {{data.device.model}} {{data.device.firmware}} {{data.started | date: 'd MMM y H:mm'}} +

+ + + + +
+
+ + +
+ +
+ + + +
+ + +
+ +
+
+ + + + diff --git a/modules/ui/src/app/progress/progress.component.scss b/modules/ui/src/app/progress/progress.component.scss new file mode 100644 index 000000000..d82596ace --- /dev/null +++ b/modules/ui/src/app/progress/progress.component.scss @@ -0,0 +1,95 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '@angular/material' as mat; +@import "../../theming/colors"; + +:host { + display: flex; + flex-direction: column; + overflow: hidden; + padding: 0 32px; +} + +.progress-content-empty { + height: 100%; + display: flex; + align-items: center; + justify-content: center; +} + +.progress-toolbar { + display: flex; + justify-content: space-between; + height: auto; + padding: 0; + background: $white; + + .toolbar-col-left { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; + padding: 0 32px 0 10px; + } + + .toolbar-row { + display: flex; + align-items: center; + + &.top { + gap: 15px; + padding: 10px 0; + + button { + flex-shrink: 0; + } + } + + &.bottom { + gap: 16px; + padding: 24px 0 8px; + + p { + margin: 0; + } + } + } + + .vertical-divider { + width: 1px; + height: 35px; + background-color: $lighter-grey; + flex-shrink: 0; + } +} + +.progress-title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.report-button, +.stop-button, +.start-button { + letter-spacing: 0.25px; + padding: 0 24px; +} + +.report-button { + background-color: mat.get-color-from-palette($color-primary, 50); + color: mat.get-color-from-palette($color-primary, 700); +} diff --git a/modules/ui/src/app/progress/progress.component.spec.ts b/modules/ui/src/app/progress/progress.component.spec.ts new file mode 100644 index 000000000..481fd4477 --- /dev/null +++ b/modules/ui/src/app/progress/progress.component.spec.ts @@ -0,0 +1,326 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ComponentFixture, discardPeriodicTasks, fakeAsync, TestBed, tick} from '@angular/core/testing'; + +import {ProgressComponent} from './progress.component'; +import {TestRunService} from '../test-run.service'; +import {of} from 'rxjs'; +import {MOCK_PROGRESS_DATA_CANCELLED, MOCK_PROGRESS_DATA_COMPLIANT, MOCK_PROGRESS_DATA_IN_PROGRESS, MOCK_PROGRESS_DATA_NOT_STARTED, TEST_DATA} from '../mocks/progress.mock'; +import {MatButtonModule} from '@angular/material/button'; +import {MatIconModule} from '@angular/material/icon'; +import {MatToolbarModule} from '@angular/material/toolbar'; +import {Component, Input} from '@angular/core'; +import {Observable} from 'rxjs/internal/Observable'; +import {IResult, TestrunStatus} from '../model/testrun-status'; +import {MatDialogModule, MatDialogRef} from '@angular/material/dialog'; +import {BehaviorSubject} from 'rxjs/internal/BehaviorSubject'; +import {Device} from '../model/device'; +import {ProgressInitiateFormComponent} from './progress-initiate-form/progress-initiate-form.component'; +import {DownloadReportComponent} from '../components/download-report/download-report.component'; + +describe('ProgressComponent', () => { + let component: ProgressComponent; + let fixture: ComponentFixture; + let compiled: HTMLElement; + let testRunServiceMock: jasmine.SpyObj; + + testRunServiceMock = jasmine.createSpyObj(['getSystemStatus', 'setSystemStatus', 'systemStatus$', 'stopTestrun', 'getDevices']); + testRunServiceMock.getDevices.and.returnValue(new BehaviorSubject([])); + + describe('Class tests', () => { + beforeEach(() => { + testRunServiceMock.systemStatus$ = of(MOCK_PROGRESS_DATA_IN_PROGRESS); + testRunServiceMock.stopTestrun.and.returnValue(of(true)); + + TestBed.configureTestingModule({ + declarations: [ + ProgressComponent, + FakeProgressBreadcrumbsComponent, + FakeProgressStatusCardComponent, + FakeProgressTableComponent + ], + providers: [ + {provide: TestRunService, useValue: testRunServiceMock}, + { + provide: MatDialogRef, + useValue: {} + },], + imports: [MatButtonModule, MatIconModule, MatToolbarModule, MatDialogModule] + }); + fixture = TestBed.createComponent(ProgressComponent); + component = fixture.componentInstance; + }); + + afterEach(() => { + testRunServiceMock.getSystemStatus.calls.reset(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('#stopTestrun should call service method stopTestrun', () => { + component.stopTestrun(); + + expect(testRunServiceMock.stopTestrun).toHaveBeenCalled(); + }) + + describe('#ngOnInit', () => { + it('should set systemStatus$ value', () => { + component.ngOnInit(); + + component.systemStatus$.subscribe(res => { + expect(res).toEqual(MOCK_PROGRESS_DATA_IN_PROGRESS) + }) + }); + + it('should set breadcrumbs$ value', () => { + const expectedResult = [ + MOCK_PROGRESS_DATA_IN_PROGRESS.device.manufacturer, + MOCK_PROGRESS_DATA_IN_PROGRESS.device.model, + MOCK_PROGRESS_DATA_IN_PROGRESS.device.firmware + ] + + component.ngOnInit(); + + component.breadcrumbs$.subscribe(res => { + expect(res).toEqual(expectedResult); + }) + }); + + it('should set dataSource$ value', () => { + const expectedResult = TEST_DATA.results; + + component.ngOnInit(); + + component.dataSource$.subscribe(res => { + expect(res).toEqual(expectedResult); + }) + }); + }); + }); + + describe('DOM tests', () => { + beforeEach(async () => { + testRunServiceMock.stopTestrun.and.returnValue(of(true)); + + await TestBed.configureTestingModule({ + declarations: [ + ProgressComponent, + FakeProgressBreadcrumbsComponent, + FakeProgressStatusCardComponent, + FakeProgressTableComponent], + providers: [ + {provide: TestRunService, useValue: testRunServiceMock}, { + provide: MatDialogRef, + useValue: {} + },], + imports: [MatButtonModule, MatIconModule, MatToolbarModule, MatDialogModule, DownloadReportComponent] + }).compileComponents(); + + fixture = TestBed.createComponent(ProgressComponent); + compiled = fixture.nativeElement as HTMLElement; + component = fixture.componentInstance; + }); + + afterEach(() => { + testRunServiceMock.getSystemStatus.calls.reset(); + }); + + describe('with not systemStatus$ data', () => { + beforeEach(() => { + (testRunServiceMock.systemStatus$ as any) = of(null); + fixture.detectChanges(); + }); + + it('should have empty state content', () => { + const emptyContentEl = compiled.querySelector('.progress-content-empty'); + const toolbarEl = compiled.querySelector('.progress-toolbar'); + + expect(emptyContentEl).not.toBeNull(); + expect(toolbarEl).toBeNull(); + }); + + it('should have enabled "Start" button', () => { + const startBtn = compiled.querySelector('.start-button') as HTMLButtonElement; + + expect(startBtn.disabled).toBeFalse(); + }); + + it('should open initiate test run modal when start button clicked', () => { + const openSpy = spyOn(component.dialog, 'open').and + .returnValue({ + afterClosed: () => of(true) + } as MatDialogRef); + const startBtn = compiled.querySelector('.start-button') as HTMLButtonElement; + startBtn.click(); + + expect(openSpy).toHaveBeenCalled(); + expect(openSpy).toHaveBeenCalledWith(ProgressInitiateFormComponent, { + autoFocus: true, + hasBackdrop: true, + disableClose: true, + panelClass: 'initiate-test-run-dialog' + }); + + openSpy.calls.reset(); + }); + }); + + describe('with available systemStatus$ data, status "In Progress"', () => { + beforeEach(() => { + testRunServiceMock.systemStatus$ = of(MOCK_PROGRESS_DATA_IN_PROGRESS); + fixture.detectChanges(); + }); + + it('should have toolbar content', () => { + const emptyContentEl = compiled.querySelector('.progress-content-empty'); + const toolbarEl = compiled.querySelector('.progress-toolbar'); + + expect(toolbarEl).not.toBeNull(); + expect(emptyContentEl).toBeNull(); + }); + + it('should have "Stop" button', () => { + const stopBtn = compiled.querySelector('.stop-button'); + + expect(stopBtn).not.toBeNull(); + }); + + it('should call stopTestrun on click "Stop" button', () => { + const stopBtn = compiled.querySelector('.stop-button') as HTMLButtonElement; + + stopBtn.click(); + + expect(testRunServiceMock.stopTestrun).toHaveBeenCalled(); + }) + + it('should have disabled "Start" button', () => { + const startBtn = compiled.querySelector('.start-button') as HTMLButtonElement; + + expect(startBtn.disabled).toBeTrue(); + }); + + it('should not have "Download Report" button', () => { + const reportBtn = compiled.querySelector('.report-button'); + + expect(reportBtn).toBeNull(); + }); + }); + + describe('pullingSystemStatusData with available status "In Progress"', () => { + it('should call again getSystemStatus)', fakeAsync(() => { + testRunServiceMock.systemStatus$ = of(MOCK_PROGRESS_DATA_IN_PROGRESS); + fixture.detectChanges(); + tick(5000); + + expect(testRunServiceMock.getSystemStatus).toHaveBeenCalledTimes(2); + discardPeriodicTasks(); + })); + }) + + describe('with available systemStatus$ data, as Completed', () => { + beforeEach(() => { + testRunServiceMock.systemStatus$ = of(MOCK_PROGRESS_DATA_COMPLIANT); + fixture.detectChanges(); + }); + + it('should not have "Stop" button', () => { + const stopBtn = compiled.querySelector('.stop-button'); + + expect(stopBtn).toBeNull(); + }); + + it('should have anable "Start" button', () => { + const startBtn = compiled.querySelector('.start-button') as HTMLButtonElement; + + expect(startBtn.disabled).toBeFalse(); + }); + + it('should have "Download Report" button', () => { + const reportBtn = compiled.querySelector('.report-button'); + + expect(reportBtn).not.toBeNull(); + }); + + it('should have report link', () => { + const link = compiled.querySelector('.download-report-link') as HTMLAnchorElement; + + expect(link.href).toEqual('https://api.testrun.io/report.pdf'); + expect(link.download).toEqual('delta_03-din-cpu_1.2.2_compliant_22_jun_2023_9:20'); + expect(link.title).toEqual('Download report for Test Run # Delta 03-DIN-CPU 1.2.2 22 Jun 2023 9:20'); + }); + }); + + describe('with available systemStatus$ data, as Cancelled', () => { + beforeEach(() => { + testRunServiceMock.systemStatus$ = of(MOCK_PROGRESS_DATA_CANCELLED); + fixture.detectChanges(); + }); + + it('should have anable "Start" button', () => { + const startBtn = compiled.querySelector('.start-button') as HTMLButtonElement; + + expect(startBtn.disabled).toBeFalse(); + }); + + it('should not have "Download Report" button', () => { + const reportBtn = compiled.querySelector('.report-button'); + + expect(reportBtn).toBeNull(); + }); + }); + + describe('with available systemStatus$ data, when Testrun not started on Idle status', () => { + beforeEach(() => { + testRunServiceMock.systemStatus$ = of(MOCK_PROGRESS_DATA_NOT_STARTED); + fixture.detectChanges(); + }); + + it('should have empty state content', () => { + const emptyContentEl = compiled.querySelector('.progress-content-empty'); + const toolbarEl = compiled.querySelector('.progress-toolbar'); + + expect(emptyContentEl).not.toBeNull(); + expect(toolbarEl).toBeNull(); + }); + }); + }); +}); + +@Component({ + selector: 'app-progress-breadcrumbs', + template: '
' +}) +class FakeProgressBreadcrumbsComponent { + @Input() breadcrumbs$!: Observable; +} + +@Component({ + selector: 'app-progress-status-card', + template: '
' +}) +class FakeProgressStatusCardComponent { + @Input() systemStatus$!: Observable; +} + +@Component({ + selector: 'app-progress-table', + template: '
' +}) +class FakeProgressTableComponent { + @Input() dataSource$!: Observable; +} diff --git a/modules/ui/src/app/progress/progress.component.ts b/modules/ui/src/app/progress/progress.component.ts new file mode 100644 index 000000000..7ed1ef14a --- /dev/null +++ b/modules/ui/src/app/progress/progress.component.ts @@ -0,0 +1,94 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {ChangeDetectionStrategy, Component, OnDestroy, OnInit} from '@angular/core'; +import {Observable} from 'rxjs/internal/Observable'; +import {TestRunService} from '../test-run.service'; +import {IDevice, IResult, StatusOfTestrun, TestrunStatus, TestsData} from '../model/testrun-status'; +import {interval, map, shareReplay, Subject, takeUntil, tap} from 'rxjs'; +import {MatDialog} from '@angular/material/dialog'; +import {ProgressInitiateFormComponent} from './progress-initiate-form/progress-initiate-form.component'; + +@Component({ + selector: 'app-progress', + templateUrl: './progress.component.html', + styleUrls: ['./progress.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush +}) +export class ProgressComponent implements OnInit, OnDestroy { + public systemStatus$!: Observable; + public breadcrumbs$!: Observable; + public dataSource$!: Observable; + public readonly StatusOfTestrun = StatusOfTestrun; + + private destroy$: Subject = new Subject(); + private startInterval = false; + + constructor(private readonly testRunService: TestRunService, public dialog: MatDialog) { + this.testRunService.getSystemStatus(); + } + + ngOnInit(): void { + this.systemStatus$ = this.testRunService.systemStatus$.pipe( + tap((res) => { + if (res.status === StatusOfTestrun.InProgress && !this.startInterval) { + this.pullingSystemStatusData(); + } + if (res.status !== StatusOfTestrun.InProgress) { + this.destroy$.next(true); + this.startInterval = false; + } + }), + shareReplay({refCount: true, bufferSize: 1}) + ); + + this.breadcrumbs$ = this.systemStatus$.pipe( + map((res: TestrunStatus) => res?.device), + map((res: IDevice) => [res?.manufacturer, res?.model, res?.firmware]) + ) + + this.dataSource$ = this.systemStatus$.pipe( + map((res: TestrunStatus) => (res.tests as TestsData)?.results) + ); + } + + private pullingSystemStatusData(): void { + this.startInterval = true; + interval(5000).pipe( + takeUntil(this.destroy$), + tap(() => this.testRunService.getSystemStatus()), + ).subscribe(); + } + + public stopTestrun(): void { + this.testRunService.stopTestrun() + .pipe(takeUntil(this.destroy$)) + .subscribe(); + } + + ngOnDestroy() { + this.destroy$.next(true); + this.destroy$.unsubscribe(); + } + + openTestRunModal(): void { + this.dialog.open(ProgressInitiateFormComponent, { + autoFocus: true, + hasBackdrop: true, + disableClose: true, + panelClass: 'initiate-test-run-dialog' + }); + } +} diff --git a/modules/ui/src/app/progress/progress.module.ts b/modules/ui/src/app/progress/progress.module.ts new file mode 100644 index 000000000..dd6729349 --- /dev/null +++ b/modules/ui/src/app/progress/progress.module.ts @@ -0,0 +1,62 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {NgModule} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {MatButtonModule} from '@angular/material/button'; +import {MatIconModule} from '@angular/material/icon'; +import {MatToolbarModule} from '@angular/material/toolbar'; +import {MatProgressBarModule} from '@angular/material/progress-bar'; +import {MatTableModule} from '@angular/material/table'; + +import {ProgressRoutingModule} from './progress-routing.module'; +import {ProgressComponent} from './progress.component'; +import {ProgressBreadcrumbsComponent} from './progress-breadcrumbs/progress-breadcrumbs.component'; +import {ProgressStatusCardComponent} from './progress-status-card/progress-status-card.component'; +import {ProgressTableComponent} from './progress-table/progress-table.component'; +import {ProgressInitiateFormComponent} from './progress-initiate-form/progress-initiate-form.component'; +import {MatDialogModule} from '@angular/material/dialog'; +import {DeviceItemComponent} from '../components/device-item/device-item.component'; +import {MatInputModule} from '@angular/material/input'; +import {ReactiveFormsModule} from '@angular/forms'; +import {DeviceTestsComponent} from '../components/device-tests/device-tests.component'; +import {DownloadReportComponent} from '../components/download-report/download-report.component'; + +@NgModule({ + declarations: [ + ProgressComponent, + ProgressBreadcrumbsComponent, + ProgressStatusCardComponent, + ProgressTableComponent, + ProgressInitiateFormComponent + ], + imports: [ + CommonModule, + ProgressRoutingModule, + MatButtonModule, + MatIconModule, + MatToolbarModule, + MatProgressBarModule, + MatTableModule, + MatDialogModule, + DeviceItemComponent, + MatInputModule, + ReactiveFormsModule, + DeviceTestsComponent, + DownloadReportComponent + ] +}) +export class ProgressModule { +} diff --git a/modules/ui/src/app/test-run.service.spec.ts b/modules/ui/src/app/test-run.service.spec.ts new file mode 100644 index 000000000..fd0c3b690 --- /dev/null +++ b/modules/ui/src/app/test-run.service.spec.ts @@ -0,0 +1,309 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {HttpClientTestingModule, HttpTestingController} from '@angular/common/http/testing'; +import {fakeAsync, getTestBed, TestBed, tick} from '@angular/core/testing'; +import {Device, TestModule} from './model/device'; + +import {TestRunService} from './test-run.service'; +import {SystemConfig} from './model/setting'; +import {MOCK_PROGRESS_DATA_IN_PROGRESS} from './mocks/progress.mock'; +import {StatusOfTestResult, TestrunStatus} from './model/testrun-status'; +import {device} from './mocks/device.mock'; + +const MOCK_SYSTEM_CONFIG: SystemConfig = { + network: { + device_intf: 'mockDeviceValue', + internet_intf: 'mockInternetValue' + } +} + +describe('TestRunService', () => { + let injector: TestBed; + let httpTestingController: HttpTestingController; + let service: TestRunService; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [HttpClientTestingModule], + providers: [TestRunService] + }); + injector = getTestBed(); + httpTestingController = injector.get(HttpTestingController); + service = injector.get(TestRunService); + }); + + afterEach(() => { + httpTestingController.verify(); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should have test modules', () => { + expect(service.getTestModules()).toEqual([ + { + displayName: "Connection", + name: "connection", + enabled: true + }, + { + displayName: "NTP", + name: "ntp", + enabled: true + }, + { + displayName: "DHCP", + name: "dhcp", + enabled: true + }, + { + displayName: "DNS", + name: "dns", + enabled: true + }, + { + displayName: "Services", + name: "nmap", + enabled: true + }, + { + displayName: "Security", + name: "security", + enabled: true + }, + { + displayName: "TLS", + name: "tls", + enabled: true + }, + ] as TestModule[]); + }); + + it('getDevices should return devices', () => { + let result: Device[] | null = null; + const deviceArray = [device] as Device[]; + + service.getDevices().subscribe((res) => { + expect(res).toEqual(result); + }); + + result = deviceArray; + service.fetchDevices(); + const req = httpTestingController.expectOne('http://localhost:8000/devices'); + + expect(req.request.method).toBe('GET'); + + req.flush(deviceArray); + }); + + it('setSystemConfig should update the systemConfig data', () => { + service.setSystemConfig(MOCK_SYSTEM_CONFIG); + + service.systemConfig$.subscribe(data => { + expect(data).toEqual(MOCK_SYSTEM_CONFIG); + }) + + }) + + it('getSystemConfig should return systemConfig data', () => { + const apiUrl = 'http://localhost:8000/system/config' + + service.getSystemConfig().subscribe((res) => { + expect(res).toEqual(MOCK_SYSTEM_CONFIG); + }); + + const req = httpTestingController.expectOne(apiUrl); + expect(req.request.method).toBe('GET'); + req.flush(MOCK_SYSTEM_CONFIG); + }); + + it('createSystemConfig should call systemConfig data', () => { + const apiUrl = 'http://localhost:8000/system/config' + + service.createSystemConfig(MOCK_SYSTEM_CONFIG).subscribe((res) => { + expect(res).toEqual({}); + }); + + const req = httpTestingController.expectOne(apiUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(MOCK_SYSTEM_CONFIG); + req.flush({}); + }); + + it('getSystemInterfaces should return array of interfaces', () => { + const apiUrl = 'http://localhost:8000/system/interfaces' + const mockSystemInterfaces: string[] = ['mockValue', 'mockValue']; + + service.getSystemInterfaces().subscribe((res) => { + expect(res).toEqual(mockSystemInterfaces); + }); + + const req = httpTestingController.expectOne(apiUrl); + expect(req.request.method).toBe('GET'); + req.flush(mockSystemInterfaces); + }); + + it('hasDevice should return true if device with mac address already exist', fakeAsync(() => { + const deviceArray = [device] as Device[]; + service.setDevices(deviceArray); + tick(); + + expect(service.hasDevice("00:1e:42:35:73:c4")).toEqual(true); + expect(service.hasDevice(" 00:1e:42:35:73:c4 ")).toEqual(true); + })); + + it('getSystemStatus should get system status data', () => { + const result = MOCK_PROGRESS_DATA_IN_PROGRESS; + + service.systemStatus$.subscribe((res) => { + expect(res).toEqual(result); + }); + + service.getSystemStatus(); + const req = httpTestingController.expectOne('http://localhost:8000/system/status'); + expect(req.request.method).toBe('GET'); + req.flush(result); + }); + + it('stopTestrun should have necessary request data', () => { + const apiUrl = 'http://localhost:8000/system/stop' + + service.stopTestrun().subscribe((res) => { + expect(res).toEqual(true); + }); + + const req = httpTestingController.expectOne(apiUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual({}); + req.flush({}); + }); + + describe('#startTestRun', () => { + it('should have necessary request data', () => { + const apiUrl = 'http://localhost:8000/system/start' + + service.startTestrun(device).subscribe((res) => { + expect(res).toEqual(true); + }); + + const req = httpTestingController.expectOne(apiUrl); + expect(req.request.method).toBe('POST'); + expect(req.request.body).toEqual(JSON.stringify({device})); + req.flush({}); + }); + + it('should have error when timeout exceeded', fakeAsync(() => { + const apiUrl = 'http://localhost:8000/system/start' + + service.startTestrun(device, 1000).subscribe(() => { + }, (error) => { + expect(error.toString()).toEqual('Timeout has occurred'); + }); + + httpTestingController.expectOne(apiUrl); + tick(1001); + })); + }); + + it('getHistory should return history', () => { + let result: TestrunStatus[] = []; + + const history = [{ + "status": "Completed", + "device": device, + "report": "https://api.testrun.io/report.pdf", + "started": "2023-06-22T10:11:00.123Z", + "finished": "2023-06-22T10:17:00.123Z", + }] as TestrunStatus[]; + + service.getHistory().subscribe((res) => { + expect(res).toEqual(result); + }); + + result = history; + service.fetchHistory(); + const req = httpTestingController.expectOne('http://localhost:8000/history'); + + expect(req.request.method).toBe('GET'); + + req.flush(history); + }); + + describe('#getResultClass', () => { + it('should return class "green" if test result is "Compliant" or "Smart Ready"', () => { + const expectedResult = { + green: true, red: false, blue: false, grey: false + }; + + const result1 = service.getResultClass(StatusOfTestResult.Compliant); + + expect(result1).toEqual(expectedResult); + }); + + it('should return class "blue" if test result is "Smart Ready" or "Informational"', () => { + const expectedResult = { + green: false, red: false, blue: true, grey: false + }; + + const result1 = service.getResultClass(StatusOfTestResult.SmartReady); + const result2 = service.getResultClass(StatusOfTestResult.Info); + + expect(result1).toEqual(expectedResult); + expect(result2).toEqual(expectedResult); + }); + + it('should return class "read" if test result is "Non Compliant" or "Error"', () => { + const expectedResult = { + green: false, red: true, blue: false, grey: false + }; + + const result = service.getResultClass(StatusOfTestResult.NonCompliant); + const result2 = service.getResultClass(StatusOfTestResult.Error); + + expect(result).toEqual(expectedResult); + expect(result2).toEqual(expectedResult); + }); + + it('should return class "grey" if test result is "Skipped" or "Not Started"', () => { + const expectedResult = { + green: false, red: false, blue: false, grey: true + }; + + const result1 = service.getResultClass(StatusOfTestResult.Skipped); + const result2 = service.getResultClass(StatusOfTestResult.NotStarted); + + expect(result1).toEqual(expectedResult); + expect(result2).toEqual(expectedResult); + }); + }); + + describe('#addDevice', () => { + it('should create array with new value if previous value is null', function () { + service.addDevice(device); + + expect(service.getDevices().value).toEqual([device]); + }); + + it('should add new value if previous value is array', function () { + service.setDevices([device, device]); + service.addDevice(device); + + expect(service.getDevices().value).toEqual([device, device, device]); + }); + + }); +}); diff --git a/modules/ui/src/app/test-run.service.ts b/modules/ui/src/app/test-run.service.ts new file mode 100644 index 000000000..a17062708 --- /dev/null +++ b/modules/ui/src/app/test-run.service.ts @@ -0,0 +1,193 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {HttpClient} from '@angular/common/http'; +import {Injectable} from '@angular/core'; +import {BehaviorSubject} from 'rxjs/internal/BehaviorSubject'; +import {Observable} from 'rxjs/internal/Observable'; +import {Device, TestModule} from './model/device'; +import {map, ReplaySubject, retry, timeout} from 'rxjs'; +import {SystemConfig} from './model/setting'; +import {StatusOfTestResult, StatusResultClassName, TestrunStatus} from './model/testrun-status'; +import {catchError} from 'rxjs/internal/operators/catchError'; +import {throwError} from 'rxjs/internal/observable/throwError'; + +const API_URL = 'http://localhost:8000' + +@Injectable({ + providedIn: 'root' +}) +export class TestRunService { + private readonly testModules: TestModule[] = [ + { + displayName: "Connection", + name: "connection", + enabled: true + }, + { + displayName: "NTP", + name: "ntp", + enabled: true + }, + { + displayName: "DHCP", + name: "dhcp", + enabled: true + }, + { + displayName: "DNS", + name: "dns", + enabled: true + }, + { + displayName: "Services", + name: "nmap", + enabled: true + }, + { + displayName: "Security", + name: "security", + enabled: true + }, + { + displayName: "TLS", + name: "tls", + enabled: true + }, + ]; + + private devices = new BehaviorSubject(null); + private _systemConfig = new BehaviorSubject({network: {}}); + public systemConfig$ = this._systemConfig.asObservable(); + private systemStatusSubject = new ReplaySubject(1); + public systemStatus$ = this.systemStatusSubject.asObservable(); + private history = new BehaviorSubject([]); + + constructor(private http: HttpClient) { + } + + getDevices(): BehaviorSubject { + return this.devices; + } + + setDevices(devices: Device[]): void { + this.devices.next(devices); + } + + setSystemConfig(config: SystemConfig): void { + this._systemConfig.next(config); + } + + setSystemStatus(status: TestrunStatus): void { + this.systemStatusSubject.next(status); + } + + fetchDevices(): void { + this.http.get(`${API_URL}/devices`).subscribe((devices: Device[]) => { + this.setDevices(devices); + }); + } + + getSystemConfig(): Observable { + return this.http + .get(`${API_URL}/system/config`) + .pipe(retry(1)) + } + + createSystemConfig(data: SystemConfig): Observable { + return this.http + .post(`${API_URL}/system/config`, data) + .pipe(retry(1)); + } + + getSystemInterfaces(): Observable { + return this.http + .get(`${API_URL}/system/interfaces`) + .pipe(retry(1)); + } + + getSystemStatus(): void { + this.http + .get(`${API_URL}/system/status`) + .subscribe((res: TestrunStatus) => { + this.setSystemStatus(res); + }); + } + + stopTestrun(): Observable { + return this.http + .post(`${API_URL}/system/stop`, {}) + .pipe(retry(1), map(() => true)); + } + + getTestModules(): TestModule[] { + return this.testModules; + } + + saveDevice(device: Device): Observable { + return this.http + .post(`${API_URL}/device`, JSON.stringify(device)) + .pipe(retry(1), map(() => true)); + } + + hasDevice(macAddress: string): boolean { + return this.devices.value?.some(device => device.mac_addr === macAddress.trim()) || false; + } + + addDevice(device: Device): void { + this.devices.next(this.devices.value ? this.devices.value.concat([device]) : [device]); + } + + updateDevice(deviceToUpdate: Device, update: Device): void { + const device = this.devices.value?.find(device => update.mac_addr === device.mac_addr)!; + device.model = update.model + device.manufacturer = update.manufacturer + device.test_modules = update.test_modules; + + this.devices.next(this.devices.value); + } + + fetchHistory(): void { + this.http + .get(`${API_URL}/history`) + .pipe(retry(1)) + .subscribe(data => { + this.history.next(data) + }); + } + + getHistory(): Observable { + return this.history; + } + + public getResultClass(result: string): StatusResultClassName { + return { + 'green': result === StatusOfTestResult.Compliant, + 'red': result === StatusOfTestResult.NonCompliant || result === StatusOfTestResult.Error, + 'blue': result === StatusOfTestResult.SmartReady || result === StatusOfTestResult.Info, + 'grey': result === StatusOfTestResult.Skipped || result === StatusOfTestResult.NotStarted + } + } + + startTestrun(device: Device, timeoutMs = 120000): Observable { + return this.http + .post(`${API_URL}/system/start`, JSON.stringify({device})) + .pipe( + timeout(timeoutMs), + map(() => true), + catchError(err => throwError(err.error?.error || err.message)) + ); + } +} diff --git a/modules/ui/src/assets/.gitkeep b/modules/ui/src/assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/modules/ui/src/assets/icons/close.svg b/modules/ui/src/assets/icons/close.svg new file mode 100644 index 000000000..ce01e8b2d --- /dev/null +++ b/modules/ui/src/assets/icons/close.svg @@ -0,0 +1,3 @@ + + + diff --git a/modules/ui/src/assets/icons/devices.svg b/modules/ui/src/assets/icons/devices.svg new file mode 100644 index 000000000..7bb8bafc7 --- /dev/null +++ b/modules/ui/src/assets/icons/devices.svg @@ -0,0 +1,5 @@ + + + diff --git a/modules/ui/src/assets/icons/devices_add.svg b/modules/ui/src/assets/icons/devices_add.svg new file mode 100644 index 000000000..1992e6298 --- /dev/null +++ b/modules/ui/src/assets/icons/devices_add.svg @@ -0,0 +1,9 @@ + + + + + + diff --git a/modules/ui/src/assets/icons/menu.svg b/modules/ui/src/assets/icons/menu.svg new file mode 100644 index 000000000..e33f5fc43 --- /dev/null +++ b/modules/ui/src/assets/icons/menu.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/modules/ui/src/assets/icons/reports.svg b/modules/ui/src/assets/icons/reports.svg new file mode 100644 index 000000000..944b31409 --- /dev/null +++ b/modules/ui/src/assets/icons/reports.svg @@ -0,0 +1,5 @@ + + + diff --git a/modules/ui/src/assets/icons/testrun_logo_color.svg b/modules/ui/src/assets/icons/testrun_logo_color.svg new file mode 100644 index 000000000..00ded5e09 --- /dev/null +++ b/modules/ui/src/assets/icons/testrun_logo_color.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + diff --git a/modules/ui/src/assets/icons/testrun_logo_small.svg b/modules/ui/src/assets/icons/testrun_logo_small.svg new file mode 100644 index 000000000..96909375b --- /dev/null +++ b/modules/ui/src/assets/icons/testrun_logo_small.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + diff --git a/modules/ui/src/favicon.ico b/modules/ui/src/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..34135c1395d25379f5d948baa9f8218bb1f39e4f GIT binary patch literal 4286 zcmeH~PiP!<6vuyy6rzWA0vd7+GUgdf*#~0U_{wJF)?fu<58!loK z;WM8DV=ZKcGG?{%SzgE!Nj+qbB_`9yb>!kcq?fl_W5nrxW&Pwjul*adGtm9w)lk^f zkm*CdNOEfaaQ4*f_o;i+mhe-r^#?VM?RCzPE4dWC7it{FTp!m_e+U~RZcq5RKIGFq zWe)aXZyEHvu7QqjknV64A3|4X0$#czuy<_@Y(+qR?!EpD|~9G zJP#l1pIaZ+^8O%yT;{{)37h;%e#V0_5)B1?@cHy}@1+No{=v_)pRVkT^n4e31g83H z`z!p8@C*6ji|FUs7e4nPKR@>S@z(a$fOWpTbva^Az2M{f#@cJo7vsR-w}AbpW%zcvSTyAMaUY2qc;5VP*L-Z_9m_51hjH4fhT#5kA@Z_xR%W zJ8d%Hqb{=JI;QQjd6=^wWjzI+tw-~K6Dbd4MUTeK8^P3m2bk-A_!2iLReX8mC~ z4&1TDF6d({c%g7JKK>@~%Q^n-9?Y?&!7azW5I?iu#fm<#J&Rw_SI$?y2V3`kue`>4 z)b7G~?rCma4EQ6_5Y(;HxA^~S@&D(4d + + + + + + + + Testrun + + + + + + + + + + + + diff --git a/modules/ui/src/main.ts b/modules/ui/src/main.ts new file mode 100644 index 000000000..e684414d0 --- /dev/null +++ b/modules/ui/src/main.ts @@ -0,0 +1,22 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import {platformBrowserDynamic} from '@angular/platform-browser-dynamic'; + +import {AppModule} from './app/app.module'; + +platformBrowserDynamic() + .bootstrapModule(AppModule) + .catch((err) => console.error(err)); diff --git a/modules/ui/src/styles.scss b/modules/ui/src/styles.scss new file mode 100644 index 000000000..3d4c65367 --- /dev/null +++ b/modules/ui/src/styles.scss @@ -0,0 +1,115 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '@angular/material' as mat; +@import './theming/theme'; + +html, body { + height: 100%; +} + +body { + margin: 0; + font-family: 'Open Sans', sans-serif; +} + +.app-sidebar-button-active .mat-icon path { + fill: $white; +} + +.app-sidebar-button.mat-mdc-icon-button .mat-mdc-button-persistent-ripple, +.app-toolbar-button.mat-mdc-icon-button .mat-mdc-button-persistent-ripple { + border-radius: inherit; +} + +.device-form-dialog, .initiate-test-run-dialog { + max-height: 100vh; +} + +.device-form-dialog .mat-mdc-dialog-container .mdc-dialog__surface { + overflow: hidden; + display: grid; + grid-template-rows: 1fr; + min-width: 300px; +} + +.device-form-dialog .mat-mdc-dialog-container { + --mdc-dialog-container-shape: 12px; + border-radius: 12px; +} + +mat-hint { + color: $grey-700; +} + +.mdc-button:focus-visible, +.mdc-icon-button:focus-visible, +.mdc-radio__native-control:focus-visible, +.mdc-checkbox__native-control:focus-visible { + outline: $black solid 2px; +} + +.mdc-button .mat-mdc-focus-indicator, +.mdc-icon-button .mat-mdc-focus-indicator { + display: none; +} + +.mdc-radio__native-control:focus:not(:focus-visible) ~ .mat-mdc-focus-indicator, +.mdc-checkbox__native-control:focus:not(:focus-visible) ~ .mat-mdc-focus-indicator { + display: none; +} + +.table-cell-result-text { + margin: 0; + padding: 4px; + border-radius: 2px; + font-size: 12px; + line-height: 16px; + letter-spacing: 0.3px; + + &.green { + background: $green-50; + color: $green-800; + } + + &.red { + background: $red-50; + color: $red-700; + } + + &.blue { + background-color: mat.get-color-from-palette($color-primary, 50); + color: mat.get-color-from-palette($color-primary, 800); + } + + &.grey { + background: $color-background-grey; + color: $grey-800; + } +} + +h2.title { + margin: 0; + font-size: 32px; + font-style: normal; + font-weight: 400; + line-height: 40px; + letter-spacing: 0; +} + +.mat-mdc-snack-bar-container { + --mat-snack-bar-button-color: #ffffff; + --mdc-snackbar-supporting-text-color: #ffffff; +} diff --git a/modules/ui/src/theming/colors.scss b/modules/ui/src/theming/colors.scss new file mode 100644 index 000000000..faf637429 --- /dev/null +++ b/modules/ui/src/theming/colors.scss @@ -0,0 +1,157 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +$black: #000000; +$white: #ffffff; +$primary: #4285F4; +$secondary: #5f6368; +$accent: #008B00; +$warn: #C5221F; +$color-background-grey: #F8F9FA; +$dark-grey: #444746; +$grey-700: #5F6368; +$grey-800: #3C4043; +$light-grey: #BDC1C6; +$lighter-grey: #DADCE0; +$green-50: #E6F4EA; +$green-800: #137333; +$red-50: #FCE8E6; +$red-700: #C5221F; +$red-800: #B31412; + +$color-primary: ( + 50: #E8F0FE, + 100: #b9dafa, + 200: #8cc3f8, + 300: #5eacf4, + 400: #3b9bf3, + 500: $primary, + 600: #1A73E8, + 700: #1967D2, + 800: #185ABC, + 900: #143b9d, + contrast: ( + 50: $black, + 100: $black, + 200: $black, + 300: $black, + 400: $white, + 500: $white, + 600: $white, + 700: $white, + 800: $white, + 900: $white + ) +); + +$color-blue-light: ( + 50 : #fcfdff, + 100 : #f8fbff, + 200 : #f4f8ff, + 300 : #eff5fe, + 400 : #ebf2fe, + 500 : #e8f0fe, + 600 : #e5eefe, + 700 : #e2ecfe, + 800 : #dee9fe, + 900 : #d8e5fd, + contrast: ( + 50 : $black, + 100 : $black, + 200 : $black, + 300 : $black, + 400 : $black, + 500 : $black, + 600 : $black, + 700 : $black, + 800 : $black, + 900 : $black + ) +); + +$color-secondary: ( + 50: #fafbfa, + 100: #f5f6f6, + 200: #f1f2f1, + 300: #e6e7e7, + 400: #c4c5c4, + 500: $secondary, + 600: #595858, + 700: #545555, + 800: #363737, + 900: #171717, + contrast: ( + 50: $black, + 100: $black, + 200: $black, + 300: $black, + 400: $white, + 500: $white, + 600: $white, + 700: $white, + 800: $white, + 900: $white + ) +); + +$color-accent: ( + 50: #e6f5e5, + 100: #CEEAD6, + 200: #9cd494, + 300: #72c568, + 400: #4fb846, + 500: $accent, + 600: #1a9d12, + 700: #188038, + 800: #007a00, + 900: #005c00, + contrast: ( + 50: $black, + 100: $black, + 200: $black, + 300: $white, + 400: $white, + 500: $white, + 600: $white, + 700: $white, + 800: $white, + 900: $white + ) +); + +$color-warn: ( + 50: #ffeaed, + 100: #ffccd0, + 200: #ef9896, + 300: #e5706d, + 400: #ee4f48, + 500: $warn, + 600: #e4342c, + 700: #C5221F, + 800: #b61312, + 900: #b61312, + contrast: ( + 50: $black, + 100: $black, + 200: $black, + 300: $black, + 400: $black, + 500: $white, + 600: $white, + 700: $white, + 800: $white, + 900: $white + ) +); diff --git a/modules/ui/src/theming/theme.scss b/modules/ui/src/theming/theme.scss new file mode 100644 index 000000000..ed5eed964 --- /dev/null +++ b/modules/ui/src/theming/theme.scss @@ -0,0 +1,68 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@use '@angular/material' as mat; +@import url('https://fonts.googleapis.com/css2?family=Open+Sans&display=swap'); +@import "colors"; + +@include mat.core(); +@include mat.strong-focus-indicators(( + border-width: 2px, +)); + + +$app-primary: mat.define-palette($color-primary); +$app-accent: mat.define-palette($color-accent); +$app-warn: mat.define-palette($color-warn); +$app-secondary: mat.define-palette($color-secondary); +$app-blue-light-palette: mat.define-palette($color-blue-light); + +$app-typography: mat.define-typography-config( + $font-family: 'Open Sans' +); + +$brand-light-theme: mat.define-light-theme(( + color: ( + primary: $app-primary, + accent: $app-accent, + warn: $app-warn, + ), + typography: $app-typography, + density: 0, +)); + +$brand-light-theme-secondary: mat.define-light-theme(( + color: ( + primary: $app-primary, + accent: $app-secondary, + warn: $app-warn, + ), + typography: $app-typography, + density: 0, +)); + +@include mat.all-component-themes($brand-light-theme); +@include mat.radio-color($brand-light-theme-secondary); +@include mat.snack-bar-typography( + mat.define-typography-config( + $font-family: 'Roboto, sans-serif' + )); +@include mat.snack-bar-color($brand-light-theme-secondary); +@include mat.checkbox-color($brand-light-theme-secondary); +@include mat.progress-bar-color((color: ( + primary: $app-blue-light-palette, + accent: $app-secondary, + warn: $app-warn, +))); diff --git a/modules/ui/src/theming/variables.scss b/modules/ui/src/theming/variables.scss new file mode 100644 index 000000000..3d5e9e53c --- /dev/null +++ b/modules/ui/src/theming/variables.scss @@ -0,0 +1,18 @@ +/** + * Copyright 2023 Google LLC + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +$device-item-width: 352px; + +$font-secondary: 'Roboto'; diff --git a/modules/ui/tsconfig.app.json b/modules/ui/tsconfig.app.json new file mode 100644 index 000000000..374cc9d29 --- /dev/null +++ b/modules/ui/tsconfig.app.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/modules/ui/tsconfig.json b/modules/ui/tsconfig.json new file mode 100644 index 000000000..ed966d43a --- /dev/null +++ b/modules/ui/tsconfig.json @@ -0,0 +1,33 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": [ + "ES2022", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/modules/ui/tsconfig.spec.json b/modules/ui/tsconfig.spec.json new file mode 100644 index 000000000..be7e9da76 --- /dev/null +++ b/modules/ui/tsconfig.spec.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/modules/ui/ui.Dockerfile b/modules/ui/ui.Dockerfile index f65f4c48b..3d8e9071b 100644 --- a/modules/ui/ui.Dockerfile +++ b/modules/ui/ui.Dockerfile @@ -13,7 +13,16 @@ # limitations under the License. # Image name: test-run/ui +FROM node:latest as build + +WORKDIR modules/ui +COPY modules/ui/ . +RUN npm install && npm run build + FROM nginx:1.25.1 -COPY modules/ui/conf/nginx.conf /etc/nginx/nginx.conf -COPY ui /usr/share/nginx/html \ No newline at end of file +COPY --from=build modules/ui/dist/ /usr/share/nginx/html + +EXPOSE 8080 + +CMD ["nginx", "-g", "daemon off;"] diff --git a/resources/devices/template/device_config.json b/resources/devices/template/device_config.json index ac8ff197c..dc6d74924 100644 --- a/resources/devices/template/device_config.json +++ b/resources/devices/template/device_config.json @@ -18,6 +18,12 @@ }, "nmap": { "enabled": true + }, + "tls": { + "enabled": true + }, + "protocol": { + "enabled": false } } } diff --git a/resources/report/Google Sans.woff2 b/resources/report/Google Sans.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..8fdba98734312af1b8560f5d429903d9994eb72d GIT binary patch literal 21360 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 Dtk8fb literal 0 HcmV?d00001 diff --git a/resources/report/testrun.png b/resources/report/testrun.png new file mode 100644 index 0000000000000000000000000000000000000000..9d0610adfb1f04ffef923de57bfb4ae9a58fb682 GIT binary patch literal 4500 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 m_EtIx;N7V&i08XH0sIef9Um6p^ft->0000 str: + """Query system status from API and returns this""" + r = requests.get(f"{API}/system/status") + response = json.loads(r.text) + return response["status"] + + +def query_test_count() -> int: + """Queries status and returns number of test results""" + r = requests.get(f"{API}/system/status") + response = json.loads(r.text) + return len(response["tests"]["results"]) + + +def start_test_device( + device_name, mac_address, image_name="ci_test_device1", args="" +): + """ Start test device container with given name """ + cmd = subprocess.run( + f"docker run -d --network=endev0 --mac-address={mac_address}" + f" --cap-add=NET_ADMIN -v /tmp:/out --privileged --name={device_name}" + f" {image_name} {args}", + shell=True, + check=True, + capture_output=True, + ) + print(cmd.stdout) + + +def stop_test_device(device_name): + """ Stop docker container with given name """ + cmd = subprocess.run( + f"docker stop {device_name}", shell=True, capture_output=True + ) + print(cmd.stdout) + cmd = subprocess.run( + f"docker rm {device_name}", shell=True, capture_output=True + ) + print(cmd.stdout) + + +def docker_logs(device_name): + """ Print docker logs from given docker container name """ + cmd = subprocess.run( + f"docker logs {device_name}", shell=True, capture_output=True + ) + print(cmd.stdout) + + +@pytest.fixture +def empty_devices_dir(): + """ Use e,pty devices directory """ + local_delete_devices(ALL_DEVICES) + + +@pytest.fixture +def testing_devices(): + """ Use devices from the testing/device_configs directory """ + local_delete_devices(ALL_DEVICES) + shutil.copytree( + os.path.join(os.path.dirname(__file__), TESTING_DEVICES), + os.path.join(TESTRUN_DIR, DEVICES_DIRECTORY), + dirs_exist_ok=True, + ) + return local_get_devices() + + +@pytest.fixture +def testrun(request): + """ Start intstance of testrun """ + test_name = request.node.originalname + proc = subprocess.Popen( + "testrun", + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + encoding="utf-8", + preexec_fn=os.setsid, + ) + + while True: + try: + outs, errs = proc.communicate(timeout=1) + except subprocess.TimeoutExpired as e: + if e.output is not None: + output = e.output.decode("utf-8") + if re.search("API waiting for requests", output): + break + except Exception as e: + pytest.fail("testrun terminated") + + time.sleep(2) + + yield + + os.killpg(os.getpgid(proc.pid), signal.SIGTERM) + try: + outs, errs = proc.communicate(timeout=60) + except Exception as e: + print(e.output) + os.killpg(os.getpgid(proc.pid), signal.SIGKILL) + pytest.exit( + "waited 60s but test run did not cleanly exit .. terminating all tests" + ) + + print(outs) + + cmd = subprocess.run( + f"docker stop $(docker ps -a -q)", shell=True, capture_output=True + ) + print(cmd.stdout) + cmd = subprocess.run( + f"docker rm $(docker ps -a -q)", shell=True, capture_output=True + ) + print(cmd.stdout) + + +def until_true(func: Callable, message: str, timeout: int): + """ Blocks until given func returns True + + Raises: + Exception if timeout has elapsed + """ + expiry_time = time.time() + timeout + while time.time() < expiry_time: + if func(): + return True + time.sleep(1) + raise Exception(f"Timed out waiting {timeout}s for {message}") + + +def dict_paths(thing: dict, stem: str = "") -> Iterator[str]: + """Returns json paths (in dot notation) from a given dictionary""" + for k, v in thing.items(): + path = f"{stem}.{k}" if stem else k + if isinstance(v, dict): + yield from dict_paths(v, path) + else: + yield path + + +def get_network_interfaces(): + """return list of network interfaces on machine + + uses /sys/class/net rather than inetfaces as test-run uses the latter + """ + path = Path("/sys/class/net") + return [i.stem for i in path.iterdir() if i.is_dir()] + + +def local_delete_devices(path): + """ Deletes all local devices + """ + devices_path = os.path.join(TESTRUN_DIR, DEVICES_DIRECTORY) + for thing in Path(devices_path).glob(path): + if thing.is_file(): + thing.unlink() + else: + shutil.rmtree(thing) + + +def local_get_devices(): + """ Returns path to device configs of devices in local/devices directory""" + return sorted( + Path(os.path.join(TESTRUN_DIR, DEVICES_DIRECTORY)).glob( + "*/device_config.json" + ) + ) + + +def test_get_system_interfaces(testrun): + """Tests API system interfaces against actual local interfaces""" + r = requests.get(f"{API}/system/interfaces") + response = json.loads(r.text) + local_interfaces = get_network_interfaces() + assert set(response) == set(local_interfaces) + + # schema expects a flat list + assert all([isinstance(x, str) for x in response]) + + +def test_modify_device(testing_devices, testrun): + with open( + os.path.join( + TESTRUN_DIR, DEVICES_DIRECTORY, testing_devices[1] + ) + ) as f: + local_device = json.load(f) + + mac_addr = local_device["mac_addr"] + new_model = "Alphabet" + + r = requests.get(f"{API}/devices") + all_devices = json.loads(r.text) + + api_device = next(x for x in all_devices if x["mac_addr"] == mac_addr) + + updated_device = copy.deepcopy(api_device) + updated_device["model"] = new_model + + new_test_modules = { + k: {"enabled": not v["enabled"]} + for k, v in updated_device["test_modules"].items() + } + updated_device["test_modules"] = new_test_modules + + print("updated_device") + pretty_print(updated_device) + print("api_device") + pretty_print(api_device) + + # update device + r = requests.post(f"{API}/device", data=json.dumps(updated_device)) + + assert r.status_code == 200 + + r = requests.get(f"{API}/devices") + all_devices = json.loads(r.text) + updated_device_api = next(x for x in all_devices if x["mac_addr"] == mac_addr) + + assert updated_device_api["model"] == new_model + assert updated_device_api["test_modules"] == new_test_modules + + +def test_create_get_devices(empty_devices_dir, testrun): + device_1 = { + "manufacturer": "Google", + "model": "First", + "mac_addr": "00:1e:42:35:73:c4", + "test_modules": { + "dns": {"enabled": True}, + "connection": {"enabled": True}, + "ntp": {"enabled": True}, + "baseline": {"enabled": True}, + "nmap": {"enabled": True}, + }, + } + + r = requests.post(f"{API}/device", data=json.dumps(device_1)) + print(r.text) + device1_response = r.text + assert r.status_code == 201 + assert len(local_get_devices()) == 1 + + device_2 = { + "manufacturer": "Google", + "model": "Second", + "mac_addr": "00:1e:42:35:73:c6", + "test_modules": { + "dns": {"enabled": True}, + "connection": {"enabled": True}, + "ntp": {"enabled": True}, + "baseline": {"enabled": True}, + "nmap": {"enabled": True}, + }, + } + r = requests.post(f"{API}/device", data=json.dumps(device_2)) + device2_response = json.loads(r.text) + assert r.status_code == 201 + assert len(local_get_devices()) == 2 + + # Test that returned devices API endpoint matches expected structure + r = requests.get(f"{API}/devices") + all_devices = json.loads(r.text) + pretty_print(all_devices) + + with open( + os.path.join(os.path.dirname(__file__), "mockito/get_devices.json") + ) as f: + mockito = json.load(f) + + print(mockito) + + # Validate structure + assert all([isinstance(x, dict) for x in all_devices]) + + # TOOO uncomment when is done + # assert set(dict_paths(mockito[0])) == set(dict_paths(all_devices[0])) + + # Validate contents of given keys matches + for key in ["mac_addr", "manufacturer", "model"]: + assert set([all_devices[0][key], all_devices[1][key]]) == set( + [device_1[key], device_2[key]] + ) + + +def test_get_system_config(testrun): + r = requests.get(f"{API}/system/config") + + with open(os.path.join(TESTRUN_DIR, SYSTEM_CONFIG_PATH)) as f: + local_config = json.load(f) + + api_config = json.loads(r.text) + + # validate structure + assert set(dict_paths(api_config)) | set(dict_paths(local_config)) == set( + dict_paths(api_config) + ) + + assert ( + local_config["network"]["device_intf"] + == api_config["network"]["device_intf"] + ) + assert ( + local_config["network"]["internet_intf"] + == api_config["network"]["internet_intf"] + ) + + +# TODO change to invalid jsdon request +@pytest.mark.skip() +def test_invalid_path_get(testrun): + r = requests.get(f"{API}/blah/blah") + response = json.loads(r.text) + assert r.status_code == 404 + with open( + os.path.join(os.path.dirname(__file__), "mockito/invalid_request.json") + ) as f: + mockito = json.load(f) + + # validate structure + assert set(dict_paths(mockito)) == set(dict_paths(response)) + + +def test_trigger_run(testing_devices, testrun): + payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} + r = requests.post(f"{API}/system/start", data=json.dumps(payload)) + assert r.status_code == 200 + + until_true( + lambda: query_system_status().lower() == "waiting for device", + "system status is `waiting for device`", + 30, + ) + + start_test_device("x123", BASELINE_MAC_ADDR) + + until_true( + lambda: query_system_status().lower() == "compliant", + "system status is `complete`", + 600, + ) + + stop_test_device("x123") + + # Validate response + r = requests.get(f"{API}/system/status") + response = json.loads(r.text) + pretty_print(response) + + # Validate results + results = {x["name"]: x for x in response["tests"]["results"]} + print(results) + # there are only 3 baseline tests + assert len(results) == 3 + + # Validate structure + with open( + os.path.join( + os.path.dirname(__file__), "mockito/running_system_status.json" + ) + ) as f: + mockito = json.load(f) + + # validate structure + assert set(dict_paths(mockito)).issubset(set(dict_paths(response))) + + # Validate results structure + assert set(dict_paths(mockito["tests"]["results"][0])).issubset( + set(dict_paths(response["tests"]["results"][0])) + ) + + # Validate a result + assert results["baseline.compliant"]["result"] == "Compliant" + +def test_stop_running_test(testing_devices, testrun): + payload = {"device": {"mac_addr": ALL_MAC_ADDR, "firmware": "asd"}} + r = requests.post(f"{API}/system/start", data=json.dumps(payload)) + assert r.status_code == 200 + + until_true( + lambda: query_system_status().lower() == "waiting for device", + "system status is `waiting for device`", + 30, + ) + + start_test_device("x12345", ALL_MAC_ADDR) + + until_true( + lambda: query_test_count() > 1, + "system status is `complete`", + 1000, + ) + + stop_test_device("x12345") + + # Validate response + r = requests.post(f"{API}/system/stop") + response = json.loads(r.text) + pretty_print(response) + assert response == {"success": "Test Run stopped"} + time.sleep(1) + # Validate response + r = requests.get(f"{API}/system/status") + response = json.loads(r.text) + pretty_print(response) + + #TODO uncomment when bug is fixed + #assert len(response["tests"]["results"]) == response["tests"]["total"] + assert len(response["tests"]["results"]) < 15 + #TODO uncomment when bug is fixed + #assert response["status"] == "Stopped" + + +@pytest.mark.skip() +def test_stop_running_not_running(testrun): + # Validate response + r = requests.post(f"{API}/system/stop") + response = json.loads(r.text) + pretty_print(response) + + assert False + +def test_multiple_runs(testing_devices, testrun): + payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} + r = requests.post(f"{API}/system/start", data=json.dumps(payload)) + assert r.status_code == 200 + print(r.text) + + until_true( + lambda: query_system_status().lower() == "waiting for device", + "system status is `waiting for device`", + 30, + ) + + start_test_device("x123", BASELINE_MAC_ADDR) + + until_true( + lambda: query_system_status().lower() == "compliant", + "system status is `complete`", + 900, + ) + + stop_test_device("x123") + + # Validate response + r = requests.get(f"{API}/system/status") + response = json.loads(r.text) + pretty_print(response) + + # Validate results + results = {x["name"]: x for x in response["tests"]["results"]} + print(results) + # there are only 3 baseline tests + assert len(results) == 3 + + payload = {"device": {"mac_addr": BASELINE_MAC_ADDR, "firmware": "asd"}} + r = requests.post(f"{API}/system/start", data=json.dumps(payload)) + # assert r.status_code == 200 + # returns 409 + print(r.text) + + until_true( + lambda: query_system_status().lower() == "waiting for device", + "system status is `waiting for device`", + 30, + ) + + start_test_device("x123", BASELINE_MAC_ADDR) + + until_true( + lambda: query_system_status().lower() == "compliant", + "system status is `complete`", + 900, + ) + + stop_test_device("x123") + +#TODO uncomment when functionality is implemented +@pytest.mark.skip() +def test_create_invalid_chars(empty_devices_dir, testrun): + # local_delete_devices(ALL_DEVICES) + # We must start test run with no devices in local/devices for this test + # to function as expected! + assert len(local_get_devices()) == 0 + + # Test adding device + device_1 = { + "manufacturer": "/'disallowed characters///", + "model": "First", + "mac_addr": BASELINE_MAC_ADDR, + "test_modules": { + "dns": {"enabled": False}, + "connection": {"enabled": True}, + "ntp": {"enabled": True}, + "baseline": {"enabled": True}, + "nmap": {"enabled": True}, + }, + } + + r = requests.post(f"{API}/device", data=json.dumps(device_1)) + print(r.text) + print(r.status_code) diff --git a/testing/baseline/system.json b/testing/baseline/system.json new file mode 100644 index 000000000..1bc6587e1 --- /dev/null +++ b/testing/baseline/system.json @@ -0,0 +1,7 @@ +{ + "network": { + "device_intf": "endev0a", + "internet_intf": "eth0" + }, + "log_level": "DEBUG" +} \ No newline at end of file diff --git a/testing/baseline/test_baseline b/testing/baseline/test_baseline index 61d0f9b56..7ecc6ba19 100755 --- a/testing/baseline/test_baseline +++ b/testing/baseline/test_baseline @@ -15,6 +15,7 @@ # limitations under the License. TESTRUN_OUT=/tmp/testrun.log +TESTRUN_DIR=/usr/local/testrun ifconfig @@ -36,23 +37,17 @@ sudo /usr/share/openvswitch/scripts/ovs-ctl start # Build Test Container sudo docker build ./testing/docker/ci_baseline -t ci1 -f ./testing/docker/ci_baseline/Dockerfile -cat <local/system.json -{ - "network": { - "device_intf": "endev0a", - "internet_intf": "eth0" - }, - "log_level": "DEBUG" -} -EOF +# Copy configuration to testrun +sudo cp testing/baseline/system.json $TESTRUN_DIR/local/system.json -sudo cmd/install +# Copy device configs to testrun +sudo cp -r testing/device_configs/* $TESTRUN_DIR/local/devices -sudo bin/testrun --single-intf --no-ui > $TESTRUN_OUT 2>&1 & +sudo testrun --single-intf --no-ui --validate > $TESTRUN_OUT 2>&1 & TPID=$! # Time to wait for testrun to be ready -WAITING=600 +WAITING=750 for i in `seq 1 $WAITING`; do if [[ -n $(fgrep "Waiting for devices on the network" $TESTRUN_OUT) ]]; then break @@ -60,7 +55,7 @@ for i in `seq 1 $WAITING`; do if [[ ! -d /proc/$TPID ]]; then cat $TESTRUN_OUT - echo "error encountered starting test run" + echo "Error encountered starting test run" exit 1 fi @@ -69,7 +64,7 @@ done if [[ $i -eq $WAITING ]]; then cat $TESTRUN_OUT - echo "failed after waiting $WAITING seconds for test-run start" + echo "Failed after waiting $WAITING seconds for testrun to start" exit 1 fi diff --git a/testing/baseline/test_baseline.py b/testing/baseline/test_baseline.py index 520f909f7..ed3bb17a1 100644 --- a/testing/baseline/test_baseline.py +++ b/testing/baseline/test_baseline.py @@ -26,6 +26,7 @@ DNS_SERVER = '10.10.10.4' CI_BASELINE_OUT = '/tmp/testrun_ci.json' +TESTRUN_DIR = '/usr/local/testrun' @pytest.fixture def container_data(): @@ -34,9 +35,7 @@ def container_data(): @pytest.fixture def validator_results(): - basedir = os.path.dirname(os.path.abspath(__file__)) - with open(os.path.join(basedir, - '../', + with open(os.path.join(TESTRUN_DIR, 'runtime/validation/faux-dev/result.json'), encoding='utf-8') as f: return json.load(f) diff --git a/testing/device_configs/only_baseline/device_config.json b/testing/device_configs/only_baseline/device_config.json new file mode 100644 index 000000000..fef1ceecf --- /dev/null +++ b/testing/device_configs/only_baseline/device_config.json @@ -0,0 +1,28 @@ +{ + "manufacturer": "Google", + "model": "Baseline", + "mac_addr": "02:42:aa:00:01:01", + "test_modules": { + "dns": { + "enabled": false + }, + "connection": { + "enabled": false + }, + "ntp": { + "enabled": false + }, + "baseline": { + "enabled": true + }, + "nmap": { + "enabled": false + }, + "tls": { + "enabled": false + }, + "protocol": { + "enabled": false + } + } +} diff --git a/testing/device_configs/tester1/device_config.json b/testing/device_configs/tester1/device_config.json index 268399b72..b979b2e26 100644 --- a/testing/device_configs/tester1/device_config.json +++ b/testing/device_configs/tester1/device_config.json @@ -4,16 +4,16 @@ "mac_addr": "02:42:aa:00:00:01", "test_modules": { "dns": { - "enabled": false + "enabled": true }, "connection": { "enabled": false }, "ntp": { - "enabled": false + "enabled": true }, "baseline": { - "enabled": false + "enabled": true }, "nmap": { "enabled": true diff --git a/testing/device_configs/tester2/device_config.json b/testing/device_configs/tester2/device_config.json index 8b090d80a..b037feb6d 100644 --- a/testing/device_configs/tester2/device_config.json +++ b/testing/device_configs/tester2/device_config.json @@ -4,16 +4,16 @@ "mac_addr": "02:42:aa:00:00:02", "test_modules": { "dns": { - "enabled": false + "enabled": true }, "connection": { - "enabled": false + "enabled": true }, "ntp": { "enabled": true }, "baseline": { - "enabled": false + "enabled": true }, "nmap": { "enabled": true diff --git a/testing/device_configs/tester3/device_config.json b/testing/device_configs/tester3/device_config.json new file mode 100644 index 000000000..b7792027e --- /dev/null +++ b/testing/device_configs/tester3/device_config.json @@ -0,0 +1,22 @@ +{ + "manufacturer": "Google", + "model": "Tester 3", + "mac_addr": "02:42:aa:00:00:03", + "test_modules": { + "dns": { + "enabled": false + }, + "connection": { + "enabled": true + }, + "ntp": { + "enabled": false + }, + "baseline": { + "enabled": true + }, + "nmap": { + "enabled": false + } + } +} diff --git a/testing/docker/ci_test_device1/Dockerfile b/testing/docker/ci_test_device1/Dockerfile index a362e2a4d..1c62d231d 100644 --- a/testing/docker/ci_test_device1/Dockerfile +++ b/testing/docker/ci_test_device1/Dockerfile @@ -6,7 +6,7 @@ ENV DEBIAN_FRONTEND=noninteractive # Update and get all additional requirements not contained in the base image RUN apt-get update && apt-get -y upgrade -RUN apt-get update && apt-get install -y isc-dhcp-client ntpdate coreutils moreutils inetutils-ping curl jq dnsutils openssl netcat-openbsd +RUN apt-get update && apt-get install -y isc-dhcp-client ntpdate coreutils moreutils inetutils-ping curl jq dnsutils openssl netcat-openbsd arping COPY entrypoint.sh /entrypoint.sh diff --git a/testing/docker/ci_test_device1/entrypoint.sh b/testing/docker/ci_test_device1/entrypoint.sh index 9152af0c8..dee51d50e 100755 --- a/testing/docker/ci_test_device1/entrypoint.sh +++ b/testing/docker/ci_test_device1/entrypoint.sh @@ -1,4 +1,4 @@ -#!/bin/bash +#!/bin/bash -x ip a @@ -17,6 +17,7 @@ OUT=/out/testrun_ci.json NTP_SERVER=10.10.10.5 DNS_SERVER=10.10.10.4 +INTF=eth0 function wout(){ temp=${1//./\".\"} @@ -30,13 +31,15 @@ function wout(){ dig @8.8.8.8 +short www.google.com # DHCP -ip addr flush dev eth0 +ip addr flush dev $INTF PID_FILE=/var/run/dhclient.pid if [ -f $PID_FILE ]; then kill -9 $(cat $PID_FILE) || true rm -f $PID_FILE fi -dhclient -v eth0 +dhclient -v $INTF +DHCP_TPID=$! +echo $DHCP_TPID if [ -n "${options[oddservices]}" ]; then @@ -108,4 +111,37 @@ if [ -n "${options[ntpv3_time_google_com]}" ]; then done) & fi +if [ -n "${options[dns_google]}" ]; then + echo starting dns requests to 8.8.8.8 + (while true; do dig @8.8.8.8 +short www.google.com; sleep 3; done) & +fi + +if [ -n "${options[dns_dhcp]}" ]; then + echo starting dns requests to $DNS_SERVER + (while true; do dig @$DNS_SERVER +short www.google.com; sleep 3; done) & +fi + +if [ -n "${options[kill_dhcp]}" ]; then + echo killing DHCP + ipv4=$(ip a show $INTF | grep "inet " | awk '{print $2}') + pkill -f dhclient + ip addr change $ipv4 dev $INTF valid_lft forever preferred_lft forever +fi + +if [ -n "${options[request_fixed]}" ]; then + ipv4=$(ip a show $INTF | grep "inet " | awk '{print $2}') + + cat <>/etc/dhcp/dhclient.conf +interface "$INTF" { + send dhcp-requested-address ${ipv4%\/*} +} +EOF +dhclient -v $INTF + +fi + + + +(while true; do arping 10.10.10.1; sleep 10; done) & +(while true; do ip a | cat; sleep 10; done) & tail -f /dev/null \ No newline at end of file diff --git a/testing/pylint/test_pylint b/testing/pylint/test_pylint index 3f4d8a3ed..6d28226cc 100755 --- a/testing/pylint/test_pylint +++ b/testing/pylint/test_pylint @@ -21,7 +21,7 @@ sudo cmd/install source venv/bin/activate sudo pip3 install pylint -files=$(find . -path ./venv -prune -o -name '*.py' -print) +files=$(find ./framework -path ./venv -prune -o -name '*.py' -print) OUT=pylint.out diff --git a/testing/tests/system.json b/testing/tests/system.json new file mode 100644 index 000000000..8717cdbfe --- /dev/null +++ b/testing/tests/system.json @@ -0,0 +1,8 @@ +{ + "network": { + "device_intf": "endev0a", + "internet_intf": "eth0" + }, + "log_level": "DEBUG", + "monitor_period": 30 +} \ No newline at end of file diff --git a/testing/tests/test_tests b/testing/tests/test_tests index 04f76daee..7277d3e7f 100755 --- a/testing/tests/test_tests +++ b/testing/tests/test_tests @@ -17,46 +17,43 @@ set -o xtrace ip a TEST_DIR=/tmp/results +TESTRUN_DIR=/usr/local/testrun MATRIX=testing/tests/test_tests.json +rm -rf $TEST_DIR/ mkdir -p $TEST_DIR # Setup requirements sudo apt-get update -sudo apt-get install openvswitch-common openvswitch-switch tcpdump jq moreutils coreutils isc-dhcp-client +sudo apt-get install -y openvswitch-common openvswitch-switch tcpdump jq moreutils coreutils isc-dhcp-client pip3 install pytest # Start OVS # Setup device network +sudo ip link add dev xyz type dummy sudo ip link add dev endev0a type veth peer name endev0b sudo ip link set dev endev0a up sudo ip link set dev endev0b up -sudo docker network create -d macvlan -o parent=endev0b endev1 +sudo docker network remove endev0 +sudo docker network create -d macvlan -o parent=endev0b endev0 sudo /usr/share/openvswitch/scripts/ovs-ctl start # Build Test Container sudo docker build ./testing/docker/ci_test_device1 -t ci_test_device1 -f ./testing/docker/ci_test_device1/Dockerfile -cat <local/system.json -{ - "network": { - "device_intf": "endev0a", - "internet_intf": "eth0" - }, - "log_level": "DEBUG", - "monitor_period": 30 -} -EOF +# Copy configuration to testrun +sudo cp testing/tests/system.json $TESTRUN_DIR/local/system.json -mkdir -p local/devices -cp -r testing/device_configs/* local/devices - -sudo cmd/install +# Copy device configs to testrun +sudo cp -r testing/device_configs/* $TESTRUN_DIR/local/devices TESTERS=$(jq -r 'keys[]' $MATRIX) for tester in $TESTERS; do + if [ -n $1 ] && [ $1 != $tester ]; then + continue + fi testrun_log=$TEST_DIR/${tester}_testrun.log device_log=$TEST_DIR/${tester}_device.log @@ -65,11 +62,12 @@ for tester in $TESTERS; do args=$(jq -r .$tester.args $MATRIX) touch $testrun_log - sudo timeout 900 bin/testrun --single-intf --no-ui --no-validate > $testrun_log 2>&1 & + sudo timeout 900 testrun --single-intf --no-ui > $testrun_log 2>&1 & TPID=$! # Time to wait for testrun to be ready - WAITING=600 + WAITING=700 + for i in `seq 1 $WAITING`; do tail -1 $testrun_log if [[ -n $(fgrep "Waiting for devices on the network" $testrun_log) ]]; then @@ -78,7 +76,7 @@ for tester in $TESTERS; do if [[ ! -d /proc/$TPID ]]; then cat $testrun_log - echo "error encountered starting test run" + echo "Error encountered starting test run" exit 1 fi @@ -87,13 +85,16 @@ for tester in $TESTERS; do if [[ $i -eq $WAITING ]]; then cat $testrun_log - echo "failed after waiting $WAITING seconds for test-run start" + echo "Failed after waiting $WAITING seconds for testrun to start" exit 1 fi + # helps unclean exits when running locally + sudo docker stop $tester && sudo docker rm $tester + # Load Test Container sudo docker run -d \ - --network=endev1 \ + --network=endev0 \ --mac-address=$ethmac \ --cap-add=NET_ADMIN \ -v /tmp:/out \ @@ -101,20 +102,35 @@ for tester in $TESTERS; do --name=$tester \ ci_test_device1 $args - wait $TPID +wait $TPID +#WAITING=600 +#for i in `seq 1 $WAITING`; do +# tail -1 $testrun_log +# if [[ -n $(fgrep "All tests complete" $testrun_log) ]]; then +# sleep 10 +# kill -9 $TPID +# fi +# +# if [[ ! -d /proc/$TPID ]]; then +# break +# fi +# +# sleep 1 +# done + + # Following line indicates that tests are completed but wait till it exits # Completed running test modules on device with mac addr 7e:41:12:d2:35:6a - #Change this line! - LOGGER.info(f"""Completed running test modules on device + # Change this line! - LOGGER.info(f"""Completed running test modules on device # with mac addr {device.mac_addr}""") - ls runtime - more runtime/network/*.log - sudo docker kill $tester + #more runtime/network/*.log | cat sudo docker logs $tester | cat - - cp runtime/test/${ethmac//:/}/report.json $TEST_DIR/$tester.json - more $TEST_DIR/$tester.json - more $testrun_log + sudo docker kill $tester && sudo docker rm $tester + + cp $TESTRUN_DIR/runtime/test/${ethmac//:/}/report.json $TEST_DIR/$tester.json + more $TEST_DIR/$tester.json | cat + more $testrun_log | cat done diff --git a/testing/tests/test_tests.json b/testing/tests/test_tests.json index 179a3f7fc..6b3251702 100644 --- a/testing/tests/test_tests.json +++ b/testing/tests/test_tests.json @@ -1,21 +1,50 @@ { "tester1": { "image": "test-run/ci_test1", - "args": "oddservices", + "args": "oddservices dns_static", "ethmac": "02:42:aa:00:00:01", "expected_results": { - "security.nmap.ports": "non-compliant" + "dns.network.hostname_resolution": "Non-Compliant", + "security.services.ftp": "Non-Compliant", + "security.services.tftp": "Non-Compliant", + "security.services.smtp": "Non-Compliant", + "security.services.pop": "Non-Compliant", + "security.services.imap": "Non-Compliant", + "ntp.network.ntp_support": "Non-Compliant", + "ntp.network.ntp_dhcp": "Non-Compliant" } }, "tester2": { + "description": "expected to pass most things", "image": "test-run/ci_test1", - "args": "ntpv4_dhcp", + "args": "ntpv4_dhcp dns_dhcp", "ethmac": "02:42:aa:00:00:02", "expected_results": { - "security.nmap.ports": "compliant", - "ntp.network.ntp_support": "compliant", - "ntp.network.ntp_dhcp": "compliant" + "security.services.ftp": "Compliant", + "security.ssh.version": "Compliant", + "security.services.telnet": "Compliant", + "security.services.smtp": "Compliant", + "security.services.http": "Compliant", + "security.services.pop": "Compliant", + "security.services.imap": "Compliant", + "security.services.snmpv3": "Compliant", + "security.services.vnc": "Compliant", + "security.services.tftp": "Compliant", + "ntp.network.ntp_server": "Compliant", + "connection.shared_address": "Compliant", + "connection.dhcp_address": "Compliant", + "connection.mac_address": "Compliant", + "connection.target_ping": "Compliant", + "connection.single_ip": "Compliant", + "connection.ipaddr.ip_change": "Compliant" } + }, + "tester3": { + "description": "", + "image": "test-run/ci_test1", + "args": "kill_dhcp", + "ethmac": "02:42:aa:00:00:03", + "expected_results": {} } } \ No newline at end of file diff --git a/testing/tests/test_tests.py b/testing/tests/test_tests.py index 1f484647a..a14afb2cb 100644 --- a/testing/tests/test_tests.py +++ b/testing/tests/test_tests.py @@ -46,10 +46,8 @@ def collect_expected_results(expected_results): def collect_actual_results(results_dict): """ Yields results from an already loaded testrun results file """ # "module"."results".[list]."result" - for maybe_module, child in results_dict.items(): - if 'results' in child and maybe_module != 'baseline': - for test in child['results']: - yield TestResult(test['name'], test['result']) + for test in results_dict.get('tests', {}).get('results', []): + yield TestResult(test['name'], test['result']) @pytest.fixture @@ -73,8 +71,7 @@ def test_tests(results, test_matrix): for tester, props in test_matrix.items(): expected = set(collect_expected_results(props['expected_results'])) actual = set(collect_actual_results(results[tester])) - - assert expected.issubset(actual), f'{tester} expected results not obtained' + assert expected & actual == expected def test_list_tests(capsys, results, test_matrix): all_tests = set(itertools.chain.from_iterable( @@ -83,19 +80,19 @@ def test_list_tests(capsys, results, test_matrix): ci_pass = set([test for testers in test_matrix.values() for test, result in testers['expected_results'].items() - if result == 'compliant']) + if result == 'Compliant']) ci_fail = set([test for testers in test_matrix.values() for test, result in testers['expected_results'].items() - if result == 'non-compliant']) + if result == 'Non-Compliant']) with capsys.disabled(): #TODO print matching the JSON schema for easy copy/paste print('============') print('============') print('tests seen:') - print('\n'.join([x.name for x in all_tests])) + print('\n'.join(set([x.name for x in all_tests]))) print('\ntesting for pass:') print('\n'.join(ci_pass)) print('\ntesting for fail:') @@ -103,7 +100,14 @@ def test_list_tests(capsys, results, test_matrix): print('\ntester results') for tester in test_matrix.keys(): print(f'\n{tester}:') + print(' expected results:') + for test in collect_expected_results(test_matrix[tester]['expected_results']): + print(f' {test.name}: {test.result}') + print(' actual results:') for test in collect_actual_results(results[tester]): - print(f'{test.name}: {test.result}') + if test.name in test_matrix[tester]['expected_results']: + print(f' {test.name}: {test.result} (exp: {test_matrix[tester]["expected_results"][test.name]})') + else: + print(f' {test.name}: {test.result}') assert True diff --git a/ui/index.html b/ui/index.html deleted file mode 100644 index 285fce5ad..000000000 --- a/ui/index.html +++ /dev/null @@ -1 +0,0 @@ -Test Run \ No newline at end of file From 3aa5bc52ec147e051e0c8e1bff4ae502b01a6b31 Mon Sep 17 00:00:00 2001 From: J Boddey Date: Mon, 2 Oct 2023 17:42:21 +0100 Subject: [PATCH 02/16] Hotfix soft launch bugs (#128) * Resolve 4x bugs * Increment version in report.pdf * Fix API test --- cmd/package | 2 +- framework/python/src/common/session.py | 7 +++---- framework/python/src/common/testreport.py | 15 ++++++++++----- .../python/src/test_orc/test_orchestrator.py | 1 - make/DEBIAN/control | 2 +- testing/api/test_api.py | 2 +- 6 files changed, 16 insertions(+), 13 deletions(-) diff --git a/cmd/package b/cmd/package index 5f24273ac..5348671fd 100755 --- a/cmd/package +++ b/cmd/package @@ -53,4 +53,4 @@ cp -r {framework,modules} $MAKE_SRC_DIR/usr/local/testrun dpkg-deb --build --root-owner-group make # Rename the .deb file -mv make.deb testrun_1-0_amd64.deb \ No newline at end of file +mv make.deb testrun_1-0-1_amd64.deb \ No newline at end of file diff --git a/framework/python/src/common/session.py b/framework/python/src/common/session.py index 638d213a8..2e69345e4 100644 --- a/framework/python/src/common/session.py +++ b/framework/python/src/common/session.py @@ -48,6 +48,7 @@ def __init__(self, config_file): self._load_config() def start(self): + self.reset() self._status = 'Waiting for Device' self._started = datetime.datetime.now() @@ -217,10 +218,8 @@ def get_total_tests(self): def reset(self): self.set_status('Idle') self.set_target_device(None) - self._tests = { - 'total': 0, - 'results': [] - } + self._total_tests = 0 + self._results = [] self._started = None self._finished = None diff --git a/framework/python/src/common/testreport.py b/framework/python/src/common/testreport.py index 02c9d65a9..792ddd22b 100644 --- a/framework/python/src/common/testreport.py +++ b/framework/python/src/common/testreport.py @@ -34,11 +34,10 @@ report_resource_dir = os.path.join(root_dir, RESOURCES_DIR) -font_file = os.path.join(report_resource_dir,'GoogleSans-Regular.ttf') -test_run_img_file = os.path.join(report_resource_dir,'testrun.png') +test_run_img_file = os.path.join(report_resource_dir, 'testrun.png') class TestReport(): - """Represents a previous Test Run report.""" + """Represents a previous Testrun report.""" def __init__(self, status='Non-Compliant', @@ -52,6 +51,7 @@ def __init__(self, self._finished = finished self._total_tests = total_tests self._results = [] + self._report_url = '' def get_status(self): return self._status @@ -80,6 +80,7 @@ def to_json(self): report_json['finished'] = self._finished.strftime(DATE_TIME_FORMAT) report_json['tests'] = {'total': self._total_tests, 'results': self._results} + report_json['report'] = self._report_url return report_json def from_json(self, json_file): @@ -88,12 +89,15 @@ def from_json(self, json_file): self._device['manufacturer'] = json_file['device']['manufacturer'] self._device['model'] = json_file['device']['model'] - if 'firmware' in self._device: + if 'firmware' in json_file['device']: self._device['firmware'] = json_file['device']['firmware'] self._status = json_file['status'] self._started = datetime.strptime(json_file['started'], DATE_TIME_FORMAT) self._finished = datetime.strptime(json_file['finished'], DATE_TIME_FORMAT) + + if 'report' in json_file: + self._report_url = json_file['report'] self._total_tests = json_file['tests']['total'] # Loop through test results @@ -162,7 +166,8 @@ def generate_pages(self,json_data): return pages def generate_page(self,json_data, page_num, max_page): - version = 'v1.0 (2023-10-02)' # Place holder until available in json report + # Placeholder until available in json report + version = 'v1.0.1 (2023-10-02)' page = '
' page += self.generate_header(json_data) if page_num == 1: diff --git a/framework/python/src/test_orc/test_orchestrator.py b/framework/python/src/test_orc/test_orchestrator.py index 6ab246b5c..d5734d0e8 100644 --- a/framework/python/src/test_orc/test_orchestrator.py +++ b/framework/python/src/test_orc/test_orchestrator.py @@ -354,7 +354,6 @@ def _run_test_module(self, module): module_results = module_results_json["results"] for test_result in module_results: self._session.add_test_result(test_result) - self._session.add_total_tests(1) except (FileNotFoundError, PermissionError, json.JSONDecodeError) as results_error: LOGGER.error( diff --git a/make/DEBIAN/control b/make/DEBIAN/control index 9ad0ed2de..ae56b91c0 100644 --- a/make/DEBIAN/control +++ b/make/DEBIAN/control @@ -1,5 +1,5 @@ Package: Testrun -Version: 1.0 +Version: 1.0.1 Architecture: amd64 Maintainer: Google Homepage: https://github.com/google/testrun diff --git a/testing/api/test_api.py b/testing/api/test_api.py index c56ef3d73..c92213ca3 100644 --- a/testing/api/test_api.py +++ b/testing/api/test_api.py @@ -450,7 +450,7 @@ def test_stop_running_test(testing_devices, testrun): r = requests.post(f"{API}/system/stop") response = json.loads(r.text) pretty_print(response) - assert response == {"success": "Test Run stopped"} + assert response == {"success": "Testrun stopped"} time.sleep(1) # Validate response r = requests.get(f"{API}/system/status") From b2cf77366a1179a1a0b34cfc8d8d8786e70c4035 Mon Sep 17 00:00:00 2001 From: Jacob Boddey Date: Tue, 3 Oct 2023 11:08:08 +0100 Subject: [PATCH 03/16] Set cancelled if stop button pressed --- framework/python/src/core/testrun.py | 1 + framework/python/src/net_orc/network_orchestrator.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/framework/python/src/core/testrun.py b/framework/python/src/core/testrun.py index d66f599e3..2695816ad 100644 --- a/framework/python/src/core/testrun.py +++ b/framework/python/src/core/testrun.py @@ -313,6 +313,7 @@ def stop(self, kill=False): self._stop_tests() self._stop_network(kill=kill) self._stop_ui() + self.get_session().set_status('Cancelled') def _register_exits(self): signal.signal(signal.SIGINT, self._exit_handler) diff --git a/framework/python/src/net_orc/network_orchestrator.py b/framework/python/src/net_orc/network_orchestrator.py index 05733dfe0..af7ff3bce 100644 --- a/framework/python/src/net_orc/network_orchestrator.py +++ b/framework/python/src/net_orc/network_orchestrator.py @@ -130,7 +130,7 @@ def get_listener(self): return self._listener def start_listener(self): - LOGGER.debug("Starting network listener") + LOGGER.debug('Starting network listener') self.get_listener().start_listener() def stop(self, kill=False): @@ -186,6 +186,7 @@ def _device_discovered(self, mac_addr): if device.ip_addr is None: LOGGER.info( f'Timed out whilst waiting for {mac_addr} to obtain an IP address') + self._session.set_status('Cancelled') return LOGGER.info( f'Device with mac addr {device.mac_addr} has obtained IP address ' From 4d43c34314a310f9d1437e5c2617a3c706812800 Mon Sep 17 00:00:00 2001 From: Jacob Boddey Date: Thu, 5 Oct 2023 11:25:09 +0100 Subject: [PATCH 04/16] Add uninstall script --- README.md | 2 ++ docs/configure_device.md | 29 --------------- docs/get_started.md | 14 +++++++- docs/network/addresses.md | 14 ++++---- framework/python/src/api/api.py | 3 ++ framework/python/src/common/session.py | 8 ----- framework/python/src/core/testrun.py | 40 ++++++--------------- local/system.json.example | 2 -- make/DEBIAN/postrm | 17 +++++++++ make/DEBIAN/prerm | 25 +++++++++++++ modules/test/conn/README.md | 3 -- modules/test/nmap/python/src/nmap_module.py | 4 +-- 12 files changed, 79 insertions(+), 82 deletions(-) delete mode 100644 docs/configure_device.md create mode 100755 make/DEBIAN/postrm create mode 100755 make/DEBIAN/prerm diff --git a/README.md b/README.md index 404de4915..7b026c2e9 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,8 @@ Testrun cannot automate everything, and so additional manual testing may be requ - Internet connection ### Software - Docker - installation guide: [https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository](https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository) +### Device + - DHCP client - The device must be able to obtain an IP address via DHCP ## Get started ▶️ Once you have met the hardware and software requirements, you can get started with Testrun by following the [Get started guide](docs/get_started.md). diff --git a/docs/configure_device.md b/docs/configure_device.md deleted file mode 100644 index 9eefcd866..000000000 --- a/docs/configure_device.md +++ /dev/null @@ -1,29 +0,0 @@ -# Device Configuration - -The device configuration file allows you to customize the testing behavior for a specific device. This file is located at `local/devices/{Device Name}/device_config.json`. Below is an overview of how to configure the device tests. - -## Device Information - -The device information section includes the manufacturer, model, and MAC address of the device. These details help identify the specific device being tested. - -## Test Modules - -Test modules are groups of tests that can be enabled or disabled as needed. You can choose which test modules to run on your device. - -### Enabling and Disabling Test Modules - -To enable or disable a test module, modify the `enabled` field within the respective module. Setting it to `true` enables the module, while setting it to `false` disables the module. - -## Customizing the Device Configuration - -To customize the device configuration for your specific device, follow these steps: - -1. Copy the default configuration file provided in the `resources/devices/template` folder. - - Create a new folder for your device under `local/devices` directory. - - Copy the `device_config.json` file from `resources/devices/template` to the newly created device folder. - -This ensures that you have a copy of the default configuration file, which you can then modify for your specific device. - -> Note: Ensure that the device configuration file is properly formatted, and the changes made align with the intended test behavior. Incorrect settings or syntax may lead to unexpected results during testing. - -If you encounter any issues or need assistance with the device configuration, refer to the Testrun documentation or ask a question on the Issues page. diff --git a/docs/get_started.md b/docs/get_started.md index f201ee8b4..9e2315940 100644 --- a/docs/get_started.md +++ b/docs/get_started.md @@ -24,6 +24,11 @@ Ensure the following software is installed on your Ubuntu LTS PC: - Build Essential - Net Tools +### Device +Any device with an ethernet connection, and support for IPv4 DHCP can be tested. + +However, to achieve a compliant test outcome, your device must be configured correctly and implement the required security features. These standards are outlined in the [Application Security Requirements for IoT Devices](https://partner-security.withgoogle.com/docs/iot_requirements). but further detail is available in [documentation for each test module](/docs/test/modules.md). + ## Installation 1. Download the latest version of the Testrun installer from the [releases page](https://github.com/google/test-run/releases) @@ -42,6 +47,8 @@ Ensure the following software is installed on your Ubuntu LTS PC: - Connect one USB Ethernet adapter to the internet source (e.g., router or switch) using an ethernet cable. - Connect the other USB Ethernet adapter directly to the IoT device you want to test using an ethernet cable. + **NOTE: The device under test should be powered off until prompted** + **NOTE: Both adapters should be disabled in the host system (IPv4, IPv6 and general). You can do this by going to Settings > Network** 2. Start Testrun. @@ -82,7 +89,9 @@ Start Testrun with the command `sudo testrun` - During testing, if you would like to stop Testrun, click 'Stop' next to the test name. -11. On completion of the test sequence, a report will appear under the history icon. +11. Once the notification 'Waiting for Device' appears, power on the device under test. + +12. On completion of the test sequence, a report will appear under the history icon. ![](/docs/ui/history_icon.png) @@ -94,3 +103,6 @@ If you encounter any issues or need assistance, consider the following: - Verify that the network interfaces are connected correctly. - Check the configuration settings. - Refer to the Testrun documentation or ask for assistance in the issues page: https://github.com/google/testrun/issues + +# Uninstall +To uninstall Testrun, use the built-in dpkg uninstall command to remove Testrun correctly. For Testrun, this would be: ```sudo apt-get remove testrun``` \ No newline at end of file diff --git a/docs/network/addresses.md b/docs/network/addresses.md index ecaacfd36..cbefc84a0 100644 --- a/docs/network/addresses.md +++ b/docs/network/addresses.md @@ -4,13 +4,13 @@ Each network service is configured with an IPv4 and IPv6 address. For IPv4 addre | Name | Mac address | IPv4 address | IPv6 address | |---------------------|----------------------|--------------|------------------------------| -| Internet gateway | 9a:02:57:1e:8f:01 | 10.10.10.1 | fd10:77be:4186::1 | -| DHCP primary | 9a:02:57:1e:8f:02 | 10.10.10.2 | fd10:77be:4186::2 | -| DHCP secondary | 9a:02:57:1e:8f:03 | 10.10.10.3 | fd10:77be:4186::3 | -| DNS server | 9a:02:57:1e:8f:04 | 10.10.10.4 | fd10:77be:4186::4 | -| NTP server | 9a:02:57:1e:8f:05 | 10.10.10.5 | fd10:77be:4186::5 | -| Radius authenticator| 9a:02:57:1e:8f:07 | 10.10.10.7 | fd10:77be:4186::7 | -| Active test module | 9a:02:57:1e:8f:09 | 10.10.10.9 | fd10:77be:4186::9 | +| Internet gateway | 9a:02:57:1e:8f:01 | 10.10.10.1 | fd10:77be:4186::1 | +| DHCP primary | 9a:02:57:1e:8f:02 | 10.10.10.2 | fd10:77be:4186::2 | +| DHCP secondary | 9a:02:57:1e:8f:03 | 10.10.10.3 | fd10:77be:4186::3 | +| DNS server | 9a:02:57:1e:8f:04 | 10.10.10.4 | fd10:77be:4186::4 | +| NTP server | 9a:02:57:1e:8f:05 | 10.10.10.5 | fd10:77be:4186::5 | +| Radius authenticator| 9a:02:57:1e:8f:07 | 10.10.10.7 | fd10:77be:4186::7 | +| Active test module | 9a:02:57:1e:8f:09 | 10.10.10.9 | fd10:77be:4186::9 | The default network range is 10.10.10.0/24 and devices will be assigned addresses in that range via DHCP. The range may change when requested by a test module. In which case, network services will be restarted and accessible on the new range, with the same final host ID. The default IPv6 network is fd10:77be:4186::/64 and addresses will be assigned to devices on the network using IPv6 SLAAC. diff --git a/framework/python/src/api/api.py b/framework/python/src/api/api.py index 1eff52525..9ffbda5ed 100644 --- a/framework/python/src/api/api.py +++ b/framework/python/src/api/api.py @@ -170,7 +170,10 @@ def _start_test_run(self): async def stop_test_run(self): LOGGER.debug("Received stop command. Stopping Testrun") + + # TODO: Set status of 'Stopping'? self._test_run.stop() + return self._generate_msg(True, "Testrun stopped") async def get_status(self): diff --git a/framework/python/src/common/session.py b/framework/python/src/common/session.py index 2e69345e4..9c84ddcd5 100644 --- a/framework/python/src/common/session.py +++ b/framework/python/src/common/session.py @@ -22,7 +22,6 @@ NETWORK_KEY = 'network' DEVICE_INTF_KEY = 'device_intf' INTERNET_INTF_KEY = 'internet_intf' -RUNTIME_KEY = 'runtime' MONITOR_PERIOD_KEY = 'monitor_period' STARTUP_TIMEOUT_KEY = 'startup_timeout' LOG_LEVEL_KEY = 'log_level' @@ -70,7 +69,6 @@ def _get_default_config(self): 'log_level': 'INFO', 'startup_timeout': 60, 'monitor_period': 30, - 'runtime': 120, 'max_device_reports': 5, 'api_port': 8000 } @@ -98,9 +96,6 @@ def _load_config(self): self._config[NETWORK_KEY][INTERNET_INTF_KEY] = config_file_json.get( NETWORK_KEY, {}).get(INTERNET_INTF_KEY) - if RUNTIME_KEY in config_file_json: - self._config[RUNTIME_KEY] = config_file_json.get(RUNTIME_KEY) - if STARTUP_TIMEOUT_KEY in config_file_json: self._config[STARTUP_TIMEOUT_KEY] = config_file_json.get( STARTUP_TIMEOUT_KEY) @@ -126,9 +121,6 @@ def _save_config(self): f.write(json.dumps(self._config, indent=2)) util.set_file_owner(owner=util.get_host_user(), path=self._config_file) - def get_runtime(self): - return self._config.get(RUNTIME_KEY) - def get_log_level(self): return self._config.get(LOG_LEVEL_KEY) diff --git a/framework/python/src/core/testrun.py b/framework/python/src/core/testrun.py index 2695816ad..f032792e6 100644 --- a/framework/python/src/core/testrun.py +++ b/framework/python/src/core/testrun.py @@ -260,20 +260,13 @@ def start(self): self._start_network() + self.get_net_orc().get_listener().register_callback( + self._device_discovered, + [NetworkEvent.DEVICE_DISCOVERED] + ) + if self._net_only: LOGGER.info('Network only option configured, no tests will be run') - - self.get_net_orc().get_listener().register_callback( - self._device_discovered, - [NetworkEvent.DEVICE_DISCOVERED] - ) - - self.get_net_orc().start_listener() - LOGGER.info('Waiting for devices on the network...') - - while True: - time.sleep(self._session.get_runtime()) - else: self._test_orc.start() @@ -282,27 +275,14 @@ def start(self): [NetworkEvent.DEVICE_STABLE] ) - self.get_net_orc().get_listener().register_callback( - self._device_discovered, - [NetworkEvent.DEVICE_DISCOVERED] - ) - - self.get_net_orc().start_listener() self._set_status('Waiting for Device') - LOGGER.info('Waiting for devices on the network...') - - time.sleep(self.get_session().get_runtime()) - if not (self._test_orc.test_in_progress() or - self.get_net_orc().monitor_in_progress()): - LOGGER.info('''Timed out whilst waiting for - device or stopping due to test completion''') - else: - while (self._test_orc.test_in_progress() or - self.get_net_orc().monitor_in_progress()): - time.sleep(5) + self.get_net_orc().start_listener() + LOGGER.info('Waiting for devices on the network...') - self.stop() + # Keep application running until stopped + while True: + time.sleep(5) def stop(self, kill=False): diff --git a/local/system.json.example b/local/system.json.example index c640669b4..2504db95d 100644 --- a/local/system.json.example +++ b/local/system.json.example @@ -4,8 +4,6 @@ "internet_intf": "enx123456789124" }, "log_level": "INFO", - "startup_timeout": 60, "monitor_period": 300, - "runtime": 120, "max_device_reports": 5 } diff --git a/make/DEBIAN/postrm b/make/DEBIAN/postrm new file mode 100755 index 000000000..b9e03bddb --- /dev/null +++ b/make/DEBIAN/postrm @@ -0,0 +1,17 @@ +#!/bin/bash -e + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +echo Finished uninstalling Testrun \ No newline at end of file diff --git a/make/DEBIAN/prerm b/make/DEBIAN/prerm new file mode 100755 index 000000000..8f0054282 --- /dev/null +++ b/make/DEBIAN/prerm @@ -0,0 +1,25 @@ +#!/bin/bash -e + +# Copyright 2023 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Remove docker images +echo Removing docker images +docker_images=$(sudo docker images --filter=reference="test-run/*" -q) + +if [ -z "$docker_images" ]; then + echo No docker images to delete +else + sudo docker rmi $docker_images > /dev/null +fi \ No newline at end of file diff --git a/modules/test/conn/README.md b/modules/test/conn/README.md index 48729f388..cf79c3efb 100644 --- a/modules/test/conn/README.md +++ b/modules/test/conn/README.md @@ -14,14 +14,11 @@ Within the ```python/src``` directory, the below tests are executed. A few dhcp | ID | Description | Expected Behavior | Required Result | |------------------------------|----------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------|-----------------| -| connection.dhcp.disconnect | The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request | The device is not set up with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds successfully to an ICMP echo (ping) request. | Required | -| connection.dhcp.disconnect_ip_change | Update device IP on the DHCP server and reconnect the device. Does the device receive the new IP address? | Device receives a new IP address within the range specified on the DHCP server. Device should respond to a ping on this new address. | Required | | connection.dhcp_address | The device under test has received an IP address from the DHCP server and responds to an ICMP echo (ping) request | The device is not set up with a static IP address. The device accepts an IP address from a DHCP server (RFC 2131) and responds successfully to an ICMP echo (ping) request. | Required | | connection.mac_address | Check and note device physical address. | N/A | Required | | connection.mac_oui | The device under test has a MAC address prefix that is registered against a known manufacturer. | The MAC address prefix is registered in the IEEE Organizationally Unique Identifier database. | Required | | connection.private_address | The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets. | The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets: 10.0.0.0 - 10.255.255.255 (10/8 prefix), 172.16.0.0 - 172.31.255.255 (172.16/12 prefix), 192.168.0.0 - 192.168.255.255 (192.168/16 prefix). | Required | | connection.shared_address | Ensure the device supports RFC 6598 IANA-Reserved IPv4 Prefix for Shared Address Space | The device under test accepts IP addresses within the range specified in RFC 6598 and communicates using these addresses. | Required | -| connection.private_address | The device under test accepts an IP address that is compliant with RFC 1918 Address Allocation for Private Internets. | The device under test accepts IP addresses within all ranges specified in RFC 1918 and communicates using these addresses. The Internet Assigned Numbers Authority (IANA) has reserved the following three blocks of the IP address space for private internets: 10.0.0.0 - 10.255.255.255.255 (10/8 prefix), 172.16.0.0 - 172.31.255.255 (172.16/12 prefix), 192.168.0.0 - 192.168.255.255 (192.168/16 prefix). | Required | | connection.single_ip | The network switch port connected to the device reports only one IP address for the device under test. | The device under test does not behave as a network switch and only requests one IP address. This test is to avoid that devices implement network switches that allow connecting strings of daisy-chained devices to one single network port, as this would not make 802.1x port-based authentication possible. | Required | | connection.target_ping | The device under test responds to an ICMP echo (ping) request. | The device under test responds to an ICMP echo (ping) request. | Required | | connection.ipaddr.ip_change | The device responds to a ping (ICMP echo request) to the new IP address it has received after the initial DHCP lease has expired. | If the lease expires before the client receives a DHCPACK, the client moves to the INIT state, MUST immediately stop any other network processing, and requires network initialization parameters as if the client were uninitialized. If the client then receives a DHCPACK allocating the client its previous network address, the client SHOULD continue network processing. If the client is given a new network address, it MUST NOT continue using the previous network address and SHOULD notify the local users of the problem. | Required | diff --git a/modules/test/nmap/python/src/nmap_module.py b/modules/test/nmap/python/src/nmap_module.py index 517dc94f9..6f684c2ef 100644 --- a/modules/test/nmap/python/src/nmap_module.py +++ b/modules/test/nmap/python/src/nmap_module.py @@ -153,7 +153,7 @@ def _check_results(self, ports, services): for open_port, open_port_info in self._scan_results.items(): for port in ports: - allowed = True if 'allowed' in port and port['allowed'] else False + allowed = True if "allowed" in port and port["allowed"] else False if (int(open_port_info["number"]) == int(port["number"]) and open_port_info["tcp_udp"] == port["type"] and open_port_info["state"] == "open"): @@ -170,7 +170,7 @@ def _check_results(self, ports, services): LOGGER.debug("Found service " + open_port_info["service"] + " on port " + str(open_port) + "/" + open_port_info["tcp_udp"]) - + if not allowed: match_ports.append(open_port_info["number"] + "/" + open_port_info["tcp_udp"]) From 1921b46c5c16179463acb7caab30c89c805026d9 Mon Sep 17 00:00:00 2001 From: Jacob Boddey Date: Thu, 5 Oct 2023 12:47:54 +0100 Subject: [PATCH 05/16] Re-add startup timeout in system.json.example --- local/system.json.example | 1 + 1 file changed, 1 insertion(+) diff --git a/local/system.json.example b/local/system.json.example index 2504db95d..d2f398b24 100644 --- a/local/system.json.example +++ b/local/system.json.example @@ -4,6 +4,7 @@ "internet_intf": "enx123456789124" }, "log_level": "INFO", + "startup_timeout": 60, "monitor_period": 300, "max_device_reports": 5 } From 9faeb8307995d96e748987187f5fc372d1293a7c Mon Sep 17 00:00:00 2001 From: Jacob Boddey Date: Thu, 5 Oct 2023 16:51:10 +0100 Subject: [PATCH 06/16] Serve report through API --- framework/python/src/api/api.py | 35 ++++++++++++++++--- .../python/src/test_orc/test_orchestrator.py | 16 ++++----- 2 files changed, 36 insertions(+), 15 deletions(-) diff --git a/framework/python/src/api/api.py b/framework/python/src/api/api.py index 9ffbda5ed..919684cce 100644 --- a/framework/python/src/api/api.py +++ b/framework/python/src/api/api.py @@ -13,9 +13,11 @@ # limitations under the License. from fastapi import FastAPI, APIRouter, Response, Request, status +from fastapi.responses import FileResponse from fastapi.middleware.cors import CORSMiddleware import json from json import JSONDecodeError +import os import psutil import threading import uvicorn @@ -29,6 +31,7 @@ DEVICE_MANUFACTURER_KEY = "manufacturer" DEVICE_MODEL_KEY = "model" DEVICE_TEST_MODULES_KEY = "test_modules" +DEVICES_PATH = "/usr/local/testrun/local/devices" class Api: """Provide REST endpoints to manage Testrun""" @@ -50,7 +53,11 @@ def __init__(self, test_run): self._router.add_api_route("/system/stop", self.stop_test_run, methods=["POST"]) self._router.add_api_route("/system/status", self.get_status) + self._router.add_api_route("/history", self.get_history) + self._router.add_api_route("/report/{device_name}/{timestamp}", + self.get_report) + self._router.add_api_route("/devices", self.get_devices) self._router.add_api_route("/device", self.save_device, methods=["POST"]) @@ -140,19 +147,24 @@ async def start_test_run(self, request: Request, response: Response): # Check if requested device is known in the device repository if device is None: response.status_code = status.HTTP_404_NOT_FOUND - return self._generate_msg(False, - "A device with that MAC address could not be found") + return self._generate_msg( + False, + "A device with that MAC address could not be found") device.firmware = body_json["device"]["firmware"] # Check Testrun is able to start if self._test_run.get_net_orc().check_config() is False: response.status_code = status.HTTP_500_INTERNAL_SERVER_ERROR - return self._generate_msg(False,"Configured interfaces are not ready for use. Ensure required interfaces are connected.") + return self._generate_msg(False,"Configured interfaces are not " + + "ready for use. Ensure required interfaces " + + "are connected.") self._test_run.get_session().reset() self._test_run.get_session().set_target_device(device) - LOGGER.info(f"Starting Testrun with device target {device.manufacturer} {device.model} with MAC address {device.mac_addr}") + LOGGER.info("Starting Testrun with device target " + + f"{device.manufacturer} {device.model} with " + + f"MAC address {device.mac_addr}") thread = threading.Thread(target=self._start_test_run, name="Testrun") @@ -221,6 +233,19 @@ async def save_device(self, request: Request, response: Response): response.status_code = status.HTTP_400_BAD_REQUEST return self._generate_msg(False, "Invalid JSON received") + async def get_report(self, response: Response, + device_name, timestamp): + + file_path = os.path.join(DEVICES_PATH, device_name, "reports", + timestamp, "report.pdf") + LOGGER.debug(f"Received get report request for {device_name} / {timestamp}") + if os.path.isfile(file_path): + return FileResponse(file_path) + else: + LOGGER.info("Report could not be found, returning 404") + response.status_code = 404 + return self._generate_msg(False, "Report could not be found") + def _validate_device_json(self, json_obj): # Check all required properties are present @@ -244,4 +269,4 @@ def _validate_device_json(self, json_obj): if char in disallowed_chars: return False - return True + return True \ No newline at end of file diff --git a/framework/python/src/test_orc/test_orchestrator.py b/framework/python/src/test_orc/test_orchestrator.py index d5734d0e8..d15ee50ff 100644 --- a/framework/python/src/test_orc/test_orchestrator.py +++ b/framework/python/src/test_orc/test_orchestrator.py @@ -31,9 +31,10 @@ TEST_MODULES_DIR = "modules/test" MODULE_CONFIG = "conf/module_config.json" LOG_REGEX = r"^[A-Z][a-z]{2} [0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} test_" -SAVED_DEVICE_REPORTS = "local/devices/{device_folder}/reports" +SAVED_DEVICE_REPORTS = "report/{device_folder}/" DEVICE_ROOT_CERTS = "local/root_certs" TESTRUN_DIR = "/usr/local/testrun" +API_URL = "http://localhost:8000" class TestOrchestrator: @@ -143,15 +144,10 @@ def _generate_report(self): "%Y-%m-%d %H:%M:%S") report["status"] = self._calculate_result() report["tests"] = self.get_session().get_report_tests() - report["report"] = "file://" + os.path.join( - TESTRUN_DIR, - SAVED_DEVICE_REPORTS.replace( - "{device_folder}", - self.get_session().get_target_device().device_folder), - self.get_session().get_finished().strftime( - "%Y-%m-%dT%H:%M:%S"), - "report.pdf" - ) + report["report"] = (API_URL + "/" + + SAVED_DEVICE_REPORTS.replace("{device_folder}", + self.get_session().get_target_device().device_folder) + + self.get_session().get_started().strftime("%Y-%m-%dT%H:%M:%S")) out_file = os.path.join( self._root_path, RUNTIME_DIR, From 218402e3b0c07a70adb15d2ac5d3279c74bc37d8 Mon Sep 17 00:00:00 2001 From: Jacob Boddey Date: Mon, 9 Oct 2023 16:04:04 +0100 Subject: [PATCH 07/16] Fix report URL --- framework/python/src/common/device.py | 10 +++- framework/python/src/common/testreport.py | 30 ++++++----- .../python/src/test_orc/test_orchestrator.py | 54 +++++++++---------- 3 files changed, 54 insertions(+), 40 deletions(-) diff --git a/framework/python/src/common/device.py b/framework/python/src/common/device.py index 5d41fbef1..47bd895bb 100644 --- a/framework/python/src/common/device.py +++ b/framework/python/src/common/device.py @@ -38,7 +38,15 @@ def add_report(self, report): def get_reports(self): return self.reports - # TODO: Add ability to remove reports once test reports have been cleaned up + def remove_report(self, timestamp): + + remove_report_target = None + for report in self.reports: + if report.get_started() == timestamp: + remove_report_target = report + + if remove_report_target is not None: + self.reports.remove(remove_report_target) def to_dict(self): """Returns the device as a python dictionary. This is used for the diff --git a/framework/python/src/common/testreport.py b/framework/python/src/common/testreport.py index 792ddd22b..e2885e113 100644 --- a/framework/python/src/common/testreport.py +++ b/framework/python/src/common/testreport.py @@ -129,18 +129,19 @@ def to_html(self): ''' def generate_test_sections(self,json_data): - results = json_data["tests"]["results"] - sections = "" + results = json_data['tests']['results'] + sections = '' for result in results: - sections += self.generate_test_section(result) + sections += self.generate_test_section(result) return sections def generate_test_section(self, result): section_content = '
\n' for key, value in result.items(): - if value is not None: # Check if the value is not None - formatted_key = key.replace('_', ' ').title() # Replace underscores and capitalize - section_content += f'

{formatted_key}: {value}

\n' + if value is not None: # Check if the value is not None + # Replace underscores and capitalize + formatted_key = key.replace('_', ' ').title() + section_content += f'

{formatted_key}: {value}

\n' section_content += '
\n
\n' return section_content @@ -231,7 +232,7 @@ def generate_result(self,result): result_html = f'''
{result['name']}
-
{result['test_description']}
+
{result['description']}
{result['result']}
''' @@ -264,7 +265,10 @@ def generate_summary(self, json_data): summary += self.generate_device_summary_label('Manufacturer',manufacturer) summary += self.generate_device_summary_label('Model',model) summary += self.generate_device_summary_label('Firmware',fw) - summary += self.generate_device_summary_label('MAC Address',mac,trailing_space=False) + summary += self.generate_device_summary_label( + 'MAC Address', + mac, + trailing_space=False) # Add the result summary summary += self.generate_result_summary(json_data) @@ -282,11 +286,13 @@ def generate_result_summary(self,json_data): result_summary += self.generate_result_summary_item('Started', json_data['started']) # Convert the timestamp strings to datetime objects - start_time = datetime.strptime(json_data['started'], "%Y-%m-%d %H:%M:%S") - end_time = datetime.strptime(json_data['finished'], "%Y-%m-%d %H:%M:%S") + start_time = datetime.strptime(json_data['started'], '%Y-%m-%d %H:%M:%S') + end_time = datetime.strptime(json_data['finished'], '%Y-%m-%d %H:%M:%S') # Calculate the duration duration = end_time - start_time - result_summary += self.generate_result_summary_item('Duration',str(duration)) + result_summary += self.generate_result_summary_item( + 'Duration', + str(duration)) result_summary += '\n
' return result_summary @@ -305,7 +311,7 @@ def generate_device_summary_label(self, key, value, trailing_space=True):
{value}
''' if trailing_space: - label += '''
''' + label += '''
''' return label def generate_head(self): diff --git a/framework/python/src/test_orc/test_orchestrator.py b/framework/python/src/test_orc/test_orchestrator.py index d15ee50ff..3e00f34a0 100644 --- a/framework/python/src/test_orc/test_orchestrator.py +++ b/framework/python/src/test_orc/test_orchestrator.py @@ -32,6 +32,7 @@ MODULE_CONFIG = "conf/module_config.json" LOG_REGEX = r"^[A-Z][a-z]{2} [0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2} test_" SAVED_DEVICE_REPORTS = "report/{device_folder}/" +LOCAL_DEVICE_REPORTS = "local/devices/{device_folder}/reports" DEVICE_ROOT_CERTS = "local/root_certs" TESTRUN_DIR = "/usr/local/testrun" API_URL = "http://localhost:8000" @@ -110,7 +111,6 @@ def run_test_modules(self): self._cleanup_old_test_results(device) LOGGER.debug("Old test results cleaned") - self._test_in_progress = False return report.get_status() @@ -120,6 +120,8 @@ def _write_reports(self, test_report): self._root_path, RUNTIME_DIR, self._session.get_target_device().mac_addr.replace(":", "")) + LOGGER.debug(f"Writing reports to {out_dir}") + # Write the json report with open(os.path.join(out_dir,"report.json"),"w", encoding="utf-8") as f: json.dump(test_report.to_json(), f, indent=2) @@ -144,22 +146,12 @@ def _generate_report(self): "%Y-%m-%d %H:%M:%S") report["status"] = self._calculate_result() report["tests"] = self.get_session().get_report_tests() - report["report"] = (API_URL + "/" + - SAVED_DEVICE_REPORTS.replace("{device_folder}", - self.get_session().get_target_device().device_folder) + - self.get_session().get_started().strftime("%Y-%m-%dT%H:%M:%S")) - - out_file = os.path.join( - self._root_path, RUNTIME_DIR, - self._session.get_target_device().mac_addr.replace(":", ""), - "report.json") - - LOGGER.debug(f"Saving report to {out_file}") - - # Write report to runtime directory - with open(out_file, "w", encoding="utf-8") as f: - json.dump(report, f, indent=2) - util.run_command(f"chown -R {self._host_user} {out_file}") + report["report"] = ( + API_URL + "/" + + SAVED_DEVICE_REPORTS.replace("{device_folder}", + self.get_session().get_target_device().device_folder) + + self.get_session().get_started().strftime("%Y-%m-%dT%H:%M:%S") + ) return report @@ -185,7 +177,7 @@ def _cleanup_old_test_results(self, device): completed_results_dir = os.path.join( self._root_path, - SAVED_DEVICE_REPORTS.replace("{device_folder}", device.device_folder)) + LOCAL_DEVICE_REPORTS.replace("{device_folder}", device.device_folder)) completed_tests = os.listdir(completed_results_dir) cur_test_count = len(completed_tests) @@ -196,8 +188,13 @@ def _cleanup_old_test_results(self, device): # Find and delete the oldest test oldest_test = self._find_oldest_test(completed_results_dir) if oldest_test is not None: - LOGGER.debug("Oldest test found, removing: " + str(oldest_test)) - shutil.rmtree(oldest_test, ignore_errors=True) + LOGGER.debug("Oldest test found, removing: " + str(oldest_test[1])) + shutil.rmtree(oldest_test[1], ignore_errors=True) + + # Remove oldest test from session + oldest_timestamp = oldest_test[0] + self.get_session().get_target_device().remove_report(oldest_timestamp) + # Confirm the delete was succesful new_test_count = len(os.listdir(completed_results_dir)) if (new_test_count != cur_test_count @@ -214,21 +211,24 @@ def _find_oldest_test(self, completed_tests_dir): oldest_timestamp = timestamp oldest_directory = completed_test if oldest_directory: - return os.path.join(completed_tests_dir, oldest_directory) + return oldest_timestamp, os.path.join(completed_tests_dir, oldest_directory) else: return None def _timestamp_results(self, device): # Define the current device results directory - cur_results_dir = os.path.join(self._root_path, RUNTIME_DIR, - device.mac_addr.replace(":", "")) + cur_results_dir = os.path.join( + self._root_path, + RUNTIME_DIR, + device.mac_addr.replace(":", "") + ) - # Define the destination results directory with timestamp - cur_time = self.get_session().get_finished().strftime("%Y-%m-%dT%H:%M:%S") completed_results_dir = os.path.join( - SAVED_DEVICE_REPORTS.replace("{device_folder}", device.device_folder), - cur_time) + self._root_path, + LOCAL_DEVICE_REPORTS.replace("{device_folder}", device.device_folder), + self.get_session().get_started().strftime("%Y-%m-%dT%H:%M:%S") + ) # Copy the results to the timestamp directory # leave current copy in place for quick reference to From a10dfba7e6079d4e7b9231ce3972d818f4603698 Mon Sep 17 00:00:00 2001 From: Jacob Boddey Date: Mon, 9 Oct 2023 20:45:44 +0100 Subject: [PATCH 08/16] Increase timeout for tests --- .github/workflows/testing.yml | 2 +- testing/device_configs/tester1/device_config.json | 10 ++++++++-- testing/device_configs/tester2/device_config.json | 10 ++++++++-- 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index 25b3a394a..eda5850a2 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -30,7 +30,7 @@ jobs: name: Tests runs-on: ubuntu-20.04 needs: testrun_baseline - timeout-minutes: 45 + timeout-minutes: 50 steps: - name: Checkout source uses: actions/checkout@v2.3.4 diff --git a/testing/device_configs/tester1/device_config.json b/testing/device_configs/tester1/device_config.json index b979b2e26..1b63594ac 100644 --- a/testing/device_configs/tester1/device_config.json +++ b/testing/device_configs/tester1/device_config.json @@ -13,10 +13,16 @@ "enabled": true }, "baseline": { - "enabled": true + "enabled": false }, "nmap": { "enabled": true + }, + "protocol": { + "enabled": false + }, + "tls": { + "enabled": false } - } + } } diff --git a/testing/device_configs/tester2/device_config.json b/testing/device_configs/tester2/device_config.json index b037feb6d..1aadfe522 100644 --- a/testing/device_configs/tester2/device_config.json +++ b/testing/device_configs/tester2/device_config.json @@ -4,7 +4,7 @@ "mac_addr": "02:42:aa:00:00:02", "test_modules": { "dns": { - "enabled": true + "enabled": false }, "connection": { "enabled": true @@ -13,10 +13,16 @@ "enabled": true }, "baseline": { - "enabled": true + "enabled": false }, "nmap": { "enabled": true + }, + "protocol": { + "enabled": false + }, + "tls": { + "enabled": false } } } From b8e1a4771d1770f7d50666203c679b9f22a049a5 Mon Sep 17 00:00:00 2001 From: Jacob Boddey Date: Wed, 11 Oct 2023 20:13:31 +0100 Subject: [PATCH 09/16] Remove tester3 --- .github/workflows/testing.yml | 2 +- testing/tests/test_tests.json | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/testing.yml b/.github/workflows/testing.yml index eda5850a2..e8a5d165e 100644 --- a/.github/workflows/testing.yml +++ b/.github/workflows/testing.yml @@ -30,7 +30,7 @@ jobs: name: Tests runs-on: ubuntu-20.04 needs: testrun_baseline - timeout-minutes: 50 + timeout-minutes: 40 steps: - name: Checkout source uses: actions/checkout@v2.3.4 diff --git a/testing/tests/test_tests.json b/testing/tests/test_tests.json index 6b3251702..51c775b0b 100644 --- a/testing/tests/test_tests.json +++ b/testing/tests/test_tests.json @@ -38,13 +38,6 @@ "connection.single_ip": "Compliant", "connection.ipaddr.ip_change": "Compliant" } - }, - "tester3": { - "description": "", - "image": "test-run/ci_test1", - "args": "kill_dhcp", - "ethmac": "02:42:aa:00:00:03", - "expected_results": {} } } \ No newline at end of file From 54e520cbaffef3781fee3227c0b4ea7d09f6c81e Mon Sep 17 00:00:00 2001 From: Jacob Boddey Date: Mon, 16 Oct 2023 08:35:47 +0100 Subject: [PATCH 10/16] Resolve test cut off --- framework/python/src/common/testreport.py | 31 ++++++++++--------- .../test/conn/python/src/connection_module.py | 19 +++--------- 2 files changed, 20 insertions(+), 30 deletions(-) diff --git a/framework/python/src/common/testreport.py b/framework/python/src/common/testreport.py index e2885e113..4241ca2c5 100644 --- a/framework/python/src/common/testreport.py +++ b/framework/python/src/common/testreport.py @@ -22,6 +22,8 @@ DATE_TIME_FORMAT = '%Y-%m-%d %H:%M:%S' RESOURCES_DIR = 'resources/report' +TESTS_FIRST_PAGE = 12 +TESTS_PER_PAGE = 20 # Locate parent directory current_dir = os.path.dirname(os.path.realpath(__file__)) @@ -146,18 +148,18 @@ def generate_test_section(self, result): return section_content def generate_pages(self,json_data): + max_page = 1 - reports_per_page = 25 # figure out how many can fit on other pages # Calculate pages test_count = len(json_data['tests']['results']) - # 10 tests can fit on the first page - if test_count > 10: - test_count -= 10 + # 12 tests can fit on the first page + if test_count > TESTS_FIRST_PAGE: + test_count -= TESTS_FIRST_PAGE - full_page = (int)(test_count / reports_per_page) - partial_page = 1 if test_count % reports_per_page > 0 else 0 + full_page = (int)(test_count / TESTS_PER_PAGE) + partial_page = 1 if test_count % TESTS_PER_PAGE > 0 else 0 if partial_page > 0: max_page += full_page + partial_page @@ -166,9 +168,9 @@ def generate_pages(self,json_data): pages += self.generate_page(json_data, i+1, max_page) return pages - def generate_page(self,json_data, page_num, max_page): + def generate_page(self, json_data, page_num, max_page): # Placeholder until available in json report - version = 'v1.0.1 (2023-10-02)' + version = 'v1.0.2 (2023-10-19)' page = '
' page += self.generate_header(json_data) if page_num == 1: @@ -178,17 +180,16 @@ def generate_page(self,json_data, page_num, max_page): page += '
' if page_num < max_page: page += '
' - #page += f'''

''' return page - def generate_body(self,json_data, page_num=1, max_page=1): + def generate_body(self, json_data, page_num=1, max_page=1): return f''' {self.generate_pages(json_data)} ''' - def generate_footer(self,page_num, max_page, version): + def generate_footer(self, page_num, max_page, version): footer = f'''