From be3937433f716184d621ad1d2cc8aaa12d80e08f Mon Sep 17 00:00:00 2001 From: Dino Chiesa Date: Fri, 27 Sep 2024 16:14:53 -0700 Subject: [PATCH 1/4] chore: refactor TD004 into multiple different plugins --- README.md | 6 +- ...TD004-targetSslInfo-enabled-and-enforce.js | 130 ++++++ lib/package/plugins/TD004-targetSslInfo.js | 203 --------- ...targetSslInfo-no-ignoreValidationErrors.js | 98 ++++ ...TD012-targetSslInfo-exactly-one-SSLInfo.js | 97 ++++ .../TD013-targetSslInfo-clientAuthEnabled.js | 121 +++++ ...-target-exactly-one-URL-or-LoadBalancer.js | 76 ++++ .../resources/TD011/fail/t1-ignore-true.xml | 9 + test/fixtures/resources/TD011/pass/foo.xml | 13 + .../resources/TD011/pass/t1-ignore-false.xml | 10 + .../TD011/pass/t2-ignore-not-present.xml | 9 + .../pass/t3-ignore-true-but-enabled-false.xml | 11 + .../resources/TD012/fail/t1-two-SSLInfos.xml | 11 + .../TD012/fail/t2-missing-SSLInfo.xml | 5 + .../fail/t3-one-SSLInfo-with-insecure-url.xml | 11 + .../resources/TD012/pass/t1-one-SSLInfo.xml | 10 + .../pass/t2-no-SSLInfo-but-insecure-url.xml | 5 + .../fail/t1-missing-keystore-keyalias.xml | 14 + .../TD013/fail/t2-missing-keyalias.xml | 14 + ...thEnabled-false-with-keystore-keyalias.xml | 12 + ...Enabled-missing-with-keystore-keyalias.xml | 14 + .../resources/TD013/pass/t1-all-good.xml | 12 + .../TD014/fail/t1-multiple-LoadBalancer.xml | 19 + .../fail/t2-with-LoadBalancer-and-URL.xml | 15 + .../fail/t3-neither-URL-nor-LoadBalancer.xml | 10 + .../resources/TD014/fail/t4-multiple-URL.xml | 12 + .../resources/TD014/fail/t5-three-URLs.xml | 12 + .../resources/TD014/pass/t1-one-URL.xml | 10 + .../TD014/pass/t2-with-LoadBalancer.xml | 15 + test/specs/TD004-sslInfo.js | 422 ------------------ test/specs/TD004-test-sslInfo.js | 212 +++++++++ test/specs/TD011-test.js | 93 ++++ test/specs/TD012-test.js | 93 ++++ test/specs/TD013-test.js | 93 ++++ test/specs/TD014-test.js | 93 ++++ 35 files changed, 1364 insertions(+), 626 deletions(-) create mode 100644 lib/package/plugins/TD004-targetSslInfo-enabled-and-enforce.js delete mode 100644 lib/package/plugins/TD004-targetSslInfo.js create mode 100644 lib/package/plugins/TD011-targetSslInfo-no-ignoreValidationErrors.js create mode 100644 lib/package/plugins/TD012-targetSslInfo-exactly-one-SSLInfo.js create mode 100644 lib/package/plugins/TD013-targetSslInfo-clientAuthEnabled.js create mode 100644 lib/package/plugins/TD014-target-exactly-one-URL-or-LoadBalancer.js create mode 100644 test/fixtures/resources/TD011/fail/t1-ignore-true.xml create mode 100644 test/fixtures/resources/TD011/pass/foo.xml create mode 100644 test/fixtures/resources/TD011/pass/t1-ignore-false.xml create mode 100644 test/fixtures/resources/TD011/pass/t2-ignore-not-present.xml create mode 100644 test/fixtures/resources/TD011/pass/t3-ignore-true-but-enabled-false.xml create mode 100644 test/fixtures/resources/TD012/fail/t1-two-SSLInfos.xml create mode 100644 test/fixtures/resources/TD012/fail/t2-missing-SSLInfo.xml create mode 100644 test/fixtures/resources/TD012/fail/t3-one-SSLInfo-with-insecure-url.xml create mode 100644 test/fixtures/resources/TD012/pass/t1-one-SSLInfo.xml create mode 100644 test/fixtures/resources/TD012/pass/t2-no-SSLInfo-but-insecure-url.xml create mode 100644 test/fixtures/resources/TD013/fail/t1-missing-keystore-keyalias.xml create mode 100644 test/fixtures/resources/TD013/fail/t2-missing-keyalias.xml create mode 100644 test/fixtures/resources/TD013/fail/t3-clientAuthEnabled-false-with-keystore-keyalias.xml create mode 100644 test/fixtures/resources/TD013/fail/t4-clientAuthEnabled-missing-with-keystore-keyalias.xml create mode 100644 test/fixtures/resources/TD013/pass/t1-all-good.xml create mode 100644 test/fixtures/resources/TD014/fail/t1-multiple-LoadBalancer.xml create mode 100644 test/fixtures/resources/TD014/fail/t2-with-LoadBalancer-and-URL.xml create mode 100644 test/fixtures/resources/TD014/fail/t3-neither-URL-nor-LoadBalancer.xml create mode 100644 test/fixtures/resources/TD014/fail/t4-multiple-URL.xml create mode 100644 test/fixtures/resources/TD014/fail/t5-three-URLs.xml create mode 100644 test/fixtures/resources/TD014/pass/t1-one-URL.xml create mode 100644 test/fixtures/resources/TD014/pass/t2-with-LoadBalancer.xml delete mode 100644 test/specs/TD004-sslInfo.js create mode 100644 test/specs/TD004-test-sslInfo.js create mode 100644 test/specs/TD011-test.js create mode 100644 test/specs/TD012-test.js create mode 100644 test/specs/TD013-test.js create mode 100644 test/specs/TD014-test.js diff --git a/README.md b/README.md index 20b1fd6..9789079 100644 --- a/README.md +++ b/README.md @@ -331,13 +331,17 @@ This is the current list: |   |:white_check_mark:| TD001 | Mgmt Server as Target | Discourage calls to the Management Server from a Proxy via target. | |   |:white_check_mark:| TD002 | Use Target Servers | Encourage the use of target servers. | |   |:white_check_mark:| TD003 | TargetEndpoint name | TargetEndpoint name should match basename of filename. | -|   |:white_check_mark:| TD004 | TargetEndpoint SSLInfo | TargetEndpoint HTTPTargetConnection should enable TLS/SSL. | +|   |:white_check_mark:| TD004 | TargetEndpoint SSLInfo | TargetEndpoint HTTPTargetConnection should enable and Enforce TLS/SSL. | |   |:white_check_mark:| TD005 | TargetEndpoint SSLInfo references | TargetEndpoint SSLInfo should use references for KeyStore and TrustStore. | |   |:white_check_mark:| TD006 | TargetEndpoint SSLInfo | When using a LoadBalancer, the SSLInfo should not be configured under HTTPTargetConnection. | |   |:white_check_mark:| TD007 | TargetEndpoint SSLInfo | TargetEndpoint HTTPTargetConnection SSLInfo should use TrustStore. | |   |:white_check_mark:| TD008 | TargetEndpoint LoadBalancer Servers | LoadBalancer should not have multiple IsFallback Server entries. | |   |:white_check_mark:| TD009 | TargetEndpoint LoadBalancer | TargetEndpoint HTTPTargetConnection should have at most one LoadBalancer. | |   |:white_check_mark:| TD010 | TargetEndpoint LoadBalancer Servers | LoadBalancer should have at least one Server entry, and no duplicate Server entries. | +|   |:white_check_mark:| TD011 | TargetEndpoint SSLInfo | TargetEndpoint HTTPTargetConnection SSLInfo should not Ignore validation errors. | +|   |:white_check_mark:| TD012 | TargetEndpoint SSLInfo | TargetEndpoint HTTPTargetConnection should have exactly one SSLInfo. | +|   |:white_check_mark:| TD013 | TargetEndpoint SSLInfo | TargetEndpoint HTTPTargetConnection should correctly configure ClientAuthEnbled. | +|   |:white_check_mark:| TD014 | TargetEndpoint SSLInfo | TargetEndpoint HTTPTargetConnection should use exctly one of URL, LoadBalancer. | | Flow |   |   |   |   | |   |:white_check_mark:| FL001 | Unconditional Flows | Only one unconditional flow will get executed. Error if more than one was detected. | | Step |   |   |   |   | diff --git a/lib/package/plugins/TD004-targetSslInfo-enabled-and-enforce.js b/lib/package/plugins/TD004-targetSslInfo-enabled-and-enforce.js new file mode 100644 index 0000000..15625a4 --- /dev/null +++ b/lib/package/plugins/TD004-targetSslInfo-enabled-and-enforce.js @@ -0,0 +1,130 @@ +/* + Copyright 2019-2024 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. +*/ + +const ruleId = require("../myUtil.js").getRuleId(); + +const plugin = { + ruleId, + name: "TargetEndpoint HTTPTargetConnection SSLInfo should use TrustStore", + message: + "TargetEndpoint HTTPTargetConnection should use TrustStore with SSLInfo.", + fatal: false, + severity: 1, // 1 = warn, 2 = error + nodeType: "Endpoint", + enabled: true +}; + +const path = require("path"), + util = require("util"), + debug = require("debug")("apigeelint:" + ruleId); + +let bundleProfile = "apigee"; +const onBundle = function (bundle, cb) { + if (bundle.profile) { + bundleProfile = bundle.profile; + } + if (typeof cb == "function") { + cb(null, false); + } +}; + +const onTargetEndpoint = function (endpoint, cb) { + const htc = endpoint.getHTTPTargetConnection(), + shortFilename = path.basename(endpoint.getFileName()); + let flagged = false; + + debug(`onTargetEndpoint shortfile(${shortFilename})`); + if (htc) { + try { + const loadBalancers = htc.select("LoadBalancer"); + if (loadBalancers.length == 0) { + const messages = []; + const sslInfos = htc.select("SSLInfo"); + if (sslInfos.length == 1) { + debug(`onTargetEndpoint sslInfos(${util.format(sslInfos)})`); + const urls = htc.select("URL"); + if (urls.length == 1) { + debug(`onTargetEndpoint url(${util.format(urls[0])})`); + + const endpointUrl = + urls[0].childNodes && + urls[0].childNodes[0] && + urls[0].childNodes[0].nodeValue; + const isHttps = endpointUrl.startsWith("https://"); + if (isHttps) { + let elts = htc.select(`SSLInfo/Enabled`); + const enabled = + elts && + elts[0] && + elts[0].childNodes && + elts[0].childNodes[0] && + elts[0].childNodes[0].nodeValue == "true"; + if (!enabled) { + messages.push( + "SSLInfo configuration does not use Enabled=true" + ); + } + + elts = htc.select(`SSLInfo/Enforce`); + let enforce = + elts && elts[0] && elts[0].childNodes && elts[0].childNodes[0]; + if (bundleProfile == "apigeex") { + enforce = enforce && enforce.nodeValue == "true"; + if (!enforce) { + messages.push( + "SSLInfo configuration does not use Enforce=true" + ); + } + } else { + if (enforce) { + messages.push( + "SSLInfo configuration must not use the Enforce element" + ); + } + } + } + } + //debug(`onTargetEndpoint messages(${messages})`); + messages.forEach((message) => { + endpoint.addMessage({ + plugin, + line: htc.getElement().lineNumber, + column: htc.getElement().columnNumber, + message + }); + debug(`onTargetEndpoint set flagged`); + flagged = true; + }); + } + } + } catch (exc1) { + console.error("exception: " + exc1); + debug(`onTargetEndpoint exc(${exc1})`); + debug(`${exc1.stack}`); + } + } + + if (typeof cb == "function") { + debug(`onTargetEndpoint isFlagged(${flagged})`); + cb(null, flagged); + } +}; + +module.exports = { + plugin, + onBundle, + onTargetEndpoint +}; diff --git a/lib/package/plugins/TD004-targetSslInfo.js b/lib/package/plugins/TD004-targetSslInfo.js deleted file mode 100644 index b3aff71..0000000 --- a/lib/package/plugins/TD004-targetSslInfo.js +++ /dev/null @@ -1,203 +0,0 @@ -/* - Copyright 2019-2024 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. -*/ - -const ruleId = require("../myUtil.js").getRuleId(); - -const plugin = { - ruleId, - name: "TargetEndpoint HTTPTargetConnection SSLInfo should use TrustStore", - message: - "TargetEndpoint HTTPTargetConnection should use TrustStore with SSLInfo.", - fatal: false, - severity: 1, // 1 = warn, 2 = error - nodeType: "Endpoint", - enabled: true -}; - -const path = require("path"), - util = require("util"), - debug = require("debug")("apigeelint:" + ruleId); - -let bundleProfile = "apigee"; -const onBundle = function (bundle, cb) { - if (bundle.profile) { - bundleProfile = bundle.profile; - } - if (typeof cb == "function") { - cb(null, false); - } -}; - -const onTargetEndpoint = function (endpoint, cb) { - const htc = endpoint.getHTTPTargetConnection(), - shortFilename = path.basename(endpoint.getFileName()); - let flagged = false; - - debug(`onTargetEndpoint shortfile(${shortFilename})`); - if (htc) { - try { - const messages = []; - const urls = htc.select("URL"); - const loadBalancers = htc.select("LoadBalancer"); - const sslInfos = htc.select("SSLInfo"); - if (sslInfos.length > 1) { - messages.push(`Incorrect multiple SSLInfo elements`); - } - if (urls.length > 1) { - messages.push(`Incorrect multiple URL elements`); - } - if (loadBalancers.length > 1) { - messages.push(`Incorrect multiple LoadBalancer elements`); - } - debug(`onTargetEndpoint sslInfos(${util.format(sslInfos)})`); - if (urls.length > 0 && loadBalancers.length > 0) { - messages.push( - `Using both URL and LoadBalancer in a proxy leads to undefined behavior` - ); - } else if (urls.length > 0) { - if (urls.length > 1) { - messages.push(`Multiple URL child elements is unsupported`); - } else { - debug(`onTargetEndpoint url(${util.format(urls[0])})`); - - const endpointUrl = - urls[0].childNodes && - urls[0].childNodes[0] && - urls[0].childNodes[0].nodeValue; - const isHttps = endpointUrl.startsWith("https://"); - if (isHttps) { - if (sslInfos.length == 0) { - messages.push(`Missing SSLInfo configuration`); - } else { - let elts = htc.select(`SSLInfo/Enabled`); - const enabled = - elts && - elts[0] && - elts[0].childNodes && - elts[0].childNodes[0] && - elts[0].childNodes[0].nodeValue == "true"; - if (!enabled) { - messages.push( - "SSLInfo configuration does not use Enabled=true" - ); - } - - elts = htc.select(`SSLInfo/Enforce`); - let enforce = - elts && elts[0] && elts[0].childNodes && elts[0].childNodes[0]; - if (bundleProfile == "apigeex") { - enforce = enforce && enforce.nodeValue == "true"; - if (!enforce) { - messages.push( - "SSLInfo configuration does not use Enforce=true" - ); - } - } else { - if (enforce) { - messages.push( - "SSLInfo configuration must not use the Enforce element" - ); - } - } - - elts = htc.select(`SSLInfo/IgnoreValidationErrors`); - const ignoreErrors = - elts && - elts[0] && - elts[0].childNodes && - elts[0].childNodes[0] && - elts[0].childNodes[0].nodeValue == "true"; - if (ignoreErrors) { - messages.push( - "SSLInfo configuration includes IgnoreValidationErrors = true" - ); - } - - elts = htc.select(`SSLInfo/ClientAuthEnabled`); - const hasClientAuth = - elts && - elts.length == 1 && - elts[0].childNodes && - elts[0].childNodes[0] && - elts[0].childNodes[0].nodeValue == "true"; - - // check for keystore, keyalias - elts = htc.select(`SSLInfo/KeyStore`); - const hasKeyStore = - elts && - elts.length == 1 && - elts[0].childNodes && - elts[0].childNodes[0]; - elts = htc.select(`SSLInfo/KeyAlias`); - const hasKeyAlias = - elts && - elts.length == 1 && - elts[0].childNodes && - elts[0].childNodes[0]; - - if (hasClientAuth && (!hasKeyStore || !hasKeyAlias)) { - messages.push( - "When ClientAuthEnabled = true, use a KeyStore and KeyAlias" - ); - } - if (!hasClientAuth && (hasKeyStore || hasKeyAlias)) { - messages.push( - "When ClientAuthEnabled = false, do not use a KeyStore and KeyAlias" - ); - } - } - } else if (sslInfos.length == 1) { - messages.push( - `SSLInfo should not be used with an insecure http url` - ); - } - } - } else if (loadBalancers.length > 0) { - debug(`using at least one LoadBalancer`); - if (loadBalancers.length > 1) { - messages.push(`Multiple LoadBalancer child elements is unsupported`); - } - } - - //debug(`onTargetEndpoint messages(${messages})`); - messages.forEach((message) => { - endpoint.addMessage({ - plugin, - line: htc.getElement().lineNumber, - column: htc.getElement().columnNumber, - message - }); - debug(`onTargetEndpoint set flagged`); - flagged = true; - }); - } catch (exc1) { - console.error("exception: " + exc1); - debug(`onTargetEndpoint exc(${exc1})`); - debug(`${exc1.stack}`); - } - } - - if (typeof cb == "function") { - debug(`onTargetEndpoint isFlagged(${flagged})`); - cb(null, flagged); - } -}; - -module.exports = { - plugin, - onBundle, - onTargetEndpoint -}; diff --git a/lib/package/plugins/TD011-targetSslInfo-no-ignoreValidationErrors.js b/lib/package/plugins/TD011-targetSslInfo-no-ignoreValidationErrors.js new file mode 100644 index 0000000..8c50703 --- /dev/null +++ b/lib/package/plugins/TD011-targetSslInfo-no-ignoreValidationErrors.js @@ -0,0 +1,98 @@ +/* + Copyright 2019-2024 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. +*/ + +const ruleId = require("../myUtil.js").getRuleId(); + +const plugin = { + ruleId, + name: "TargetEndpoint HTTPTargetConnection SSLInfo should not ignore errors", + message: "SSLInfo configuration includes IgnoreValidationErrors = true", + + fatal: false, + severity: 1, // 1 = warn, 2 = error + nodeType: "Endpoint", + enabled: true +}; + +const path = require("path"), + util = require("util"), + debug = require("debug")("apigeelint:" + ruleId); + +const onTargetEndpoint = function (endpoint, cb) { + const htc = endpoint.getHTTPTargetConnection(), + shortFilename = path.basename(endpoint.getFileName()); + let flagged = false; + + debug(`onTargetEndpoint shortfile(${shortFilename})`); + if (htc) { + try { + const urls = htc.select("URL"); + if (urls.length == 1) { + debug(`onTargetEndpoint url(${util.format(urls[0])})`); + const sslInfos = htc.select("SSLInfo"); + if (sslInfos.length == 1) { + debug(`onTargetEndpoint sslInfos(${util.format(sslInfos)})`); + const endpointUrl = + urls[0].childNodes && + urls[0].childNodes[0] && + urls[0].childNodes[0].nodeValue; + const isHttps = endpointUrl.startsWith("https://"); + if (isHttps) { + let elts = htc.select(`SSLInfo/Enabled`); + const enabled = + elts && + elts[0] && + elts[0].childNodes && + elts[0].childNodes[0] && + elts[0].childNodes[0].nodeValue == "true"; + if (elts.length == 0 || enabled) { + elts = htc.select(`SSLInfo/IgnoreValidationErrors`); + const ignoreErrors = + elts && + elts[0] && + elts[0].childNodes && + elts[0].childNodes[0] && + elts[0].childNodes[0].nodeValue == "true"; + if (ignoreErrors) { + endpoint.addMessage({ + plugin, + line: htc.getElement().lineNumber, + column: htc.getElement().columnNumber, + message: plugin.message + }); + flagged = true; + } + } + } + } + } + } catch (exc1) { + console.error("exception: " + exc1); + debug(`onTargetEndpoint exc(${exc1})`); + debug(`${exc1.stack}`); + } + } + + if (typeof cb == "function") { + debug(`onTargetEndpoint isFlagged(${flagged})`); + cb(null, flagged); + } +}; + +module.exports = { + plugin, + onTargetEndpoint +}; diff --git a/lib/package/plugins/TD012-targetSslInfo-exactly-one-SSLInfo.js b/lib/package/plugins/TD012-targetSslInfo-exactly-one-SSLInfo.js new file mode 100644 index 0000000..952d3bf --- /dev/null +++ b/lib/package/plugins/TD012-targetSslInfo-exactly-one-SSLInfo.js @@ -0,0 +1,97 @@ +/* + Copyright 2019-2024 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. +*/ + +const ruleId = require("../myUtil.js").getRuleId(); + +const plugin = { + ruleId, + name: "TargetEndpoint HTTPTargetConnection should have exactly one SSLInfo", + message: + "TargetEndpoint HTTPTargetConnection should have exactly one SSLInfo", + fatal: false, + severity: 1, // 1 = warn, 2 = error + nodeType: "Endpoint", + enabled: true +}; + +const path = require("path"), + util = require("util"), + debug = require("debug")("apigeelint:" + ruleId); + +const onTargetEndpoint = function (endpoint, cb) { + const htc = endpoint.getHTTPTargetConnection(), + shortFilename = path.basename(endpoint.getFileName()); + let flagged = false; + + debug(`onTargetEndpoint shortfile(${shortFilename})`); + if (htc) { + try { + const urls = htc.select("URL"); + if (urls.length == 1) { + const sslInfos = htc.select("SSLInfo"); + const endpointUrl = + urls[0].childNodes && + urls[0].childNodes[0] && + urls[0].childNodes[0].nodeValue; + const isHttps = endpointUrl.startsWith("https://"); + if (isHttps) { + debug( + `onTargetEndpoint sslInfos.length(${util.format(sslInfos.length)})` + ); + if (sslInfos.length == 0) { + endpoint.addMessage({ + plugin, + line: htc.getElement().lineNumber, + column: htc.getElement().columnNumber, + message: plugin.message + }); + flagged = true; + } else if (sslInfos.length > 1) { + endpoint.addMessage({ + plugin, + line: sslInfos[1].lineNumber, + column: sslInfos[1].columnNumber, + message: plugin.message + }); + flagged = true; + } + } else if (sslInfos.length == 1) { + endpoint.addMessage({ + plugin, + line: sslInfos[0].lineNumber, + column: sslInfos[0].columnNumber, + message: `SSLInfo should not be used with an insecure http url` + }); + flagged = true; + } + } + } catch (exc1) { + console.error("exception: " + exc1); + debug(`onTargetEndpoint exc(${exc1})`); + debug(`${exc1.stack}`); + } + } + + if (typeof cb == "function") { + debug(`onTargetEndpoint isFlagged(${flagged})`); + cb(null, flagged); + } +}; + +module.exports = { + plugin, + onTargetEndpoint +}; diff --git a/lib/package/plugins/TD013-targetSslInfo-clientAuthEnabled.js b/lib/package/plugins/TD013-targetSslInfo-clientAuthEnabled.js new file mode 100644 index 0000000..f1d4d8a --- /dev/null +++ b/lib/package/plugins/TD013-targetSslInfo-clientAuthEnabled.js @@ -0,0 +1,121 @@ +/* + Copyright 2019-2024 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. +*/ + +const ruleId = require("../myUtil.js").getRuleId(); + +const plugin = { + ruleId, + name: "TargetEndpoint HTTPTargetConnection SSLInfo ClientAuthEnabled hygiene", + message: + "TargetEndpoint HTTPTargetConnection SSLInfo ClientAuthEnabled hygiene", + fatal: false, + severity: 1, // 1 = warn, 2 = error + nodeType: "Endpoint", + enabled: true +}; + +const path = require("path"), + util = require("util"), + debug = require("debug")("apigeelint:" + ruleId); + +const onTargetEndpoint = function (endpoint, cb) { + const htc = endpoint.getHTTPTargetConnection(), + shortFilename = path.basename(endpoint.getFileName()); + let flagged = false; + + debug(`onTargetEndpoint shortfile(${shortFilename})`); + if (htc) { + try { + const loadBalancers = htc.select("LoadBalancer"); + if (loadBalancers.length == 0) { + const messages = []; + const sslInfos = htc.select("SSLInfo"); + if (sslInfos.length == 1) { + debug(`onTargetEndpoint sslInfos(${util.format(sslInfos)})`); + const urls = htc.select("URL"); + if (urls.length == 1) { + debug(`onTargetEndpoint url(${util.format(urls[0])})`); + + const endpointUrl = + urls[0].childNodes && + urls[0].childNodes[0] && + urls[0].childNodes[0].nodeValue; + const isHttps = endpointUrl.startsWith("https://"); + if (isHttps) { + let elts = htc.select(`SSLInfo/ClientAuthEnabled`); + const hasClientAuth = + elts && + elts.length == 1 && + elts[0].childNodes && + elts[0].childNodes[0] && + elts[0].childNodes[0].nodeValue == "true"; + + // check for keystore, keyalias + elts = htc.select(`SSLInfo/KeyStore`); + const hasKeyStore = + elts && + elts.length == 1 && + elts[0].childNodes && + elts[0].childNodes[0]; + elts = htc.select(`SSLInfo/KeyAlias`); + const hasKeyAlias = + elts && + elts.length == 1 && + elts[0].childNodes && + elts[0].childNodes[0]; + + if (hasClientAuth && (!hasKeyStore || !hasKeyAlias)) { + messages.push( + "When ClientAuthEnabled = true, use a KeyStore and KeyAlias" + ); + } + if (!hasClientAuth && (hasKeyStore || hasKeyAlias)) { + messages.push( + "When ClientAuthEnabled = false, do not use a KeyStore and KeyAlias" + ); + } + } + } + //debug(`onTargetEndpoint messages(${messages})`); + messages.forEach((message) => { + endpoint.addMessage({ + plugin, + line: htc.getElement().lineNumber, + column: htc.getElement().columnNumber, + message + }); + debug(`onTargetEndpoint set flagged`); + flagged = true; + }); + } + } + } catch (exc1) { + console.error("exception: " + exc1); + debug(`onTargetEndpoint exc(${exc1})`); + debug(`${exc1.stack}`); + } + } + + if (typeof cb == "function") { + debug(`onTargetEndpoint isFlagged(${flagged})`); + cb(null, flagged); + } +}; + +module.exports = { + plugin, + onTargetEndpoint +}; diff --git a/lib/package/plugins/TD014-target-exactly-one-URL-or-LoadBalancer.js b/lib/package/plugins/TD014-target-exactly-one-URL-or-LoadBalancer.js new file mode 100644 index 0000000..a284b64 --- /dev/null +++ b/lib/package/plugins/TD014-target-exactly-one-URL-or-LoadBalancer.js @@ -0,0 +1,76 @@ +/* + Copyright 2019-2024 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. +*/ + +const ruleId = require("../myUtil.js").getRuleId(); + +const plugin = { + ruleId, + name: "TargetEndpoint HTTPTargetConnection URL and LoadBalancer", + message: + "TargetEndpoint HTTPTargetConnection should have exactly one of {URL, LoadBalancer}", + fatal: false, + severity: 1, // 1 = warn, 2 = error + nodeType: "Endpoint", + enabled: true +}; + +const path = require("path"), + debug = require("debug")("apigeelint:" + ruleId); + +const onTargetEndpoint = function (endpoint, cb) { + const htc = endpoint.getHTTPTargetConnection(), + shortFilename = path.basename(endpoint.getFileName()); + let flagged = false; + + debug(`onTargetEndpoint shortfile(${shortFilename})`); + if (htc) { + try { + const urls = htc.select("URL"); + const loadBalancers = htc.select("LoadBalancer"); + debug( + `onTargetEndpoint urls(${urls.length}) loadBalancers(${loadBalancers.length})` + ); + if ( + (loadBalancers.length == 0 && urls.length == 0) || + (loadBalancers.length > 0 && urls.length > 0) || + loadBalancers.length > 1 || + urls.length > 1 + ) { + endpoint.addMessage({ + plugin, + line: htc.getElement().lineNumber, + column: htc.getElement().columnNumber, + message: plugin.message + }); + flagged = true; + } + } catch (exc1) { + console.error("exception: " + exc1); + debug(`onTargetEndpoint exc(${exc1})`); + debug(`${exc1.stack}`); + } + } + + if (typeof cb == "function") { + debug(`onTargetEndpoint isFlagged(${flagged})`); + cb(null, flagged); + } +}; + +module.exports = { + plugin, + onTargetEndpoint +}; diff --git a/test/fixtures/resources/TD011/fail/t1-ignore-true.xml b/test/fixtures/resources/TD011/fail/t1-ignore-true.xml new file mode 100644 index 0000000..fa0aff6 --- /dev/null +++ b/test/fixtures/resources/TD011/fail/t1-ignore-true.xml @@ -0,0 +1,9 @@ + + + + true + true + + https://foo.com/apis/{api_name}/maskconfigs + + diff --git a/test/fixtures/resources/TD011/pass/foo.xml b/test/fixtures/resources/TD011/pass/foo.xml new file mode 100644 index 0000000..27cad43 --- /dev/null +++ b/test/fixtures/resources/TD011/pass/foo.xml @@ -0,0 +1,13 @@ + + + + true + false + truststore1 + + + + + + + diff --git a/test/fixtures/resources/TD011/pass/t1-ignore-false.xml b/test/fixtures/resources/TD011/pass/t1-ignore-false.xml new file mode 100644 index 0000000..e5679b3 --- /dev/null +++ b/test/fixtures/resources/TD011/pass/t1-ignore-false.xml @@ -0,0 +1,10 @@ + + + + true + false + truststore1 + + https://foo.com/apis/{api_name}/maskconfigs + + diff --git a/test/fixtures/resources/TD011/pass/t2-ignore-not-present.xml b/test/fixtures/resources/TD011/pass/t2-ignore-not-present.xml new file mode 100644 index 0000000..5ed7aa6 --- /dev/null +++ b/test/fixtures/resources/TD011/pass/t2-ignore-not-present.xml @@ -0,0 +1,9 @@ + + + + true + truststore1 + + https://foo.com/apis/{api_name}/maskconfigs + + diff --git a/test/fixtures/resources/TD011/pass/t3-ignore-true-but-enabled-false.xml b/test/fixtures/resources/TD011/pass/t3-ignore-true-but-enabled-false.xml new file mode 100644 index 0000000..daa4f6b --- /dev/null +++ b/test/fixtures/resources/TD011/pass/t3-ignore-true-but-enabled-false.xml @@ -0,0 +1,11 @@ + + + + + false + true + truststore1 + + https://foo.com/apis/{api_name}/maskconfigs + + diff --git a/test/fixtures/resources/TD012/fail/t1-two-SSLInfos.xml b/test/fixtures/resources/TD012/fail/t1-two-SSLInfos.xml new file mode 100644 index 0000000..9a912bc --- /dev/null +++ b/test/fixtures/resources/TD012/fail/t1-two-SSLInfos.xml @@ -0,0 +1,11 @@ + + + + true + true + truststore1 + + + https://foo.com/apis/{api_name}/maskconfigs + + diff --git a/test/fixtures/resources/TD012/fail/t2-missing-SSLInfo.xml b/test/fixtures/resources/TD012/fail/t2-missing-SSLInfo.xml new file mode 100644 index 0000000..a4da943 --- /dev/null +++ b/test/fixtures/resources/TD012/fail/t2-missing-SSLInfo.xml @@ -0,0 +1,5 @@ + + + https://foo.com/apis/{api_name}/maskconfigs + + diff --git a/test/fixtures/resources/TD012/fail/t3-one-SSLInfo-with-insecure-url.xml b/test/fixtures/resources/TD012/fail/t3-one-SSLInfo-with-insecure-url.xml new file mode 100644 index 0000000..35f7ded --- /dev/null +++ b/test/fixtures/resources/TD012/fail/t3-one-SSLInfo-with-insecure-url.xml @@ -0,0 +1,11 @@ + + + + true + true + truststore1 + + + http://foo.com/apis/{api_name}/maskconfigs + + diff --git a/test/fixtures/resources/TD012/pass/t1-one-SSLInfo.xml b/test/fixtures/resources/TD012/pass/t1-one-SSLInfo.xml new file mode 100644 index 0000000..ace20a8 --- /dev/null +++ b/test/fixtures/resources/TD012/pass/t1-one-SSLInfo.xml @@ -0,0 +1,10 @@ + + + + true + true + truststore1 + + https://foo.com/apis/{api_name}/maskconfigs + + diff --git a/test/fixtures/resources/TD012/pass/t2-no-SSLInfo-but-insecure-url.xml b/test/fixtures/resources/TD012/pass/t2-no-SSLInfo-but-insecure-url.xml new file mode 100644 index 0000000..0bb67e1 --- /dev/null +++ b/test/fixtures/resources/TD012/pass/t2-no-SSLInfo-but-insecure-url.xml @@ -0,0 +1,5 @@ + + + http://foo.com/apis/{api_name}/maskconfigs + + diff --git a/test/fixtures/resources/TD013/fail/t1-missing-keystore-keyalias.xml b/test/fixtures/resources/TD013/fail/t1-missing-keystore-keyalias.xml new file mode 100644 index 0000000..00c8c16 --- /dev/null +++ b/test/fixtures/resources/TD013/fail/t1-missing-keystore-keyalias.xml @@ -0,0 +1,14 @@ + + + + true + true + truststore1 + + + https://foo.com/apis/{api_name}/maskconfigs + + diff --git a/test/fixtures/resources/TD013/fail/t2-missing-keyalias.xml b/test/fixtures/resources/TD013/fail/t2-missing-keyalias.xml new file mode 100644 index 0000000..a99f23f --- /dev/null +++ b/test/fixtures/resources/TD013/fail/t2-missing-keyalias.xml @@ -0,0 +1,14 @@ + + + + true + true + truststore1 + keystore1 + + + https://foo.com/apis/{api_name}/maskconfigs + + diff --git a/test/fixtures/resources/TD013/fail/t3-clientAuthEnabled-false-with-keystore-keyalias.xml b/test/fixtures/resources/TD013/fail/t3-clientAuthEnabled-false-with-keystore-keyalias.xml new file mode 100644 index 0000000..c85f034 --- /dev/null +++ b/test/fixtures/resources/TD013/fail/t3-clientAuthEnabled-false-with-keystore-keyalias.xml @@ -0,0 +1,12 @@ + + + + true + false + truststore1 + keystore1 + key1 + + https://foo.com/apis/{api_name}/maskconfigs + + diff --git a/test/fixtures/resources/TD013/fail/t4-clientAuthEnabled-missing-with-keystore-keyalias.xml b/test/fixtures/resources/TD013/fail/t4-clientAuthEnabled-missing-with-keystore-keyalias.xml new file mode 100644 index 0000000..aa9c7f7 --- /dev/null +++ b/test/fixtures/resources/TD013/fail/t4-clientAuthEnabled-missing-with-keystore-keyalias.xml @@ -0,0 +1,14 @@ + + + + true + + truststore1 + keystore1 + key1 + + https://foo.com/apis/{api_name}/maskconfigs + + diff --git a/test/fixtures/resources/TD013/pass/t1-all-good.xml b/test/fixtures/resources/TD013/pass/t1-all-good.xml new file mode 100644 index 0000000..da72c53 --- /dev/null +++ b/test/fixtures/resources/TD013/pass/t1-all-good.xml @@ -0,0 +1,12 @@ + + + + true + true + truststore1 + keystore1 + key1 + + https://foo.com/apis/{api_name}/maskconfigs + + diff --git a/test/fixtures/resources/TD014/fail/t1-multiple-LoadBalancer.xml b/test/fixtures/resources/TD014/fail/t1-multiple-LoadBalancer.xml new file mode 100644 index 0000000..ffbe67a --- /dev/null +++ b/test/fixtures/resources/TD014/fail/t1-multiple-LoadBalancer.xml @@ -0,0 +1,19 @@ + + + + true + false + truststore1 + + + + + + + + + + + + + diff --git a/test/fixtures/resources/TD014/fail/t2-with-LoadBalancer-and-URL.xml b/test/fixtures/resources/TD014/fail/t2-with-LoadBalancer-and-URL.xml new file mode 100644 index 0000000..a33f79c --- /dev/null +++ b/test/fixtures/resources/TD014/fail/t2-with-LoadBalancer-and-URL.xml @@ -0,0 +1,15 @@ + + + + true + false + truststore1 + + + + + + + https://foo.bar + + diff --git a/test/fixtures/resources/TD014/fail/t3-neither-URL-nor-LoadBalancer.xml b/test/fixtures/resources/TD014/fail/t3-neither-URL-nor-LoadBalancer.xml new file mode 100644 index 0000000..a65eed5 --- /dev/null +++ b/test/fixtures/resources/TD014/fail/t3-neither-URL-nor-LoadBalancer.xml @@ -0,0 +1,10 @@ + + + + true + true + truststore1 + + + + diff --git a/test/fixtures/resources/TD014/fail/t4-multiple-URL.xml b/test/fixtures/resources/TD014/fail/t4-multiple-URL.xml new file mode 100644 index 0000000..b9d190a --- /dev/null +++ b/test/fixtures/resources/TD014/fail/t4-multiple-URL.xml @@ -0,0 +1,12 @@ + + + + true + false + truststore1 + + + https://foo.bar + https://foo.bam + + diff --git a/test/fixtures/resources/TD014/fail/t5-three-URLs.xml b/test/fixtures/resources/TD014/fail/t5-three-URLs.xml new file mode 100644 index 0000000..b2f2cef --- /dev/null +++ b/test/fixtures/resources/TD014/fail/t5-three-URLs.xml @@ -0,0 +1,12 @@ + + + https://foo.com/apis/{api_name}/maskconfigs + + true + true + truststore1 + + https://foo.com/apis/{api_name}/maskconfigs + https://bar.com/apis/{api_name}/maskconfigs + + diff --git a/test/fixtures/resources/TD014/pass/t1-one-URL.xml b/test/fixtures/resources/TD014/pass/t1-one-URL.xml new file mode 100644 index 0000000..ace20a8 --- /dev/null +++ b/test/fixtures/resources/TD014/pass/t1-one-URL.xml @@ -0,0 +1,10 @@ + + + + true + true + truststore1 + + https://foo.com/apis/{api_name}/maskconfigs + + diff --git a/test/fixtures/resources/TD014/pass/t2-with-LoadBalancer.xml b/test/fixtures/resources/TD014/pass/t2-with-LoadBalancer.xml new file mode 100644 index 0000000..2d19934 --- /dev/null +++ b/test/fixtures/resources/TD014/pass/t2-with-LoadBalancer.xml @@ -0,0 +1,15 @@ + + + + true + false + truststore1 + + + + + + + + + diff --git a/test/specs/TD004-sslInfo.js b/test/specs/TD004-sslInfo.js deleted file mode 100644 index 4afad1d..0000000 --- a/test/specs/TD004-sslInfo.js +++ /dev/null @@ -1,422 +0,0 @@ -/* - Copyright 2019-2024 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. -*/ -/* global describe, it, configuration */ - -const assert = require("assert"), - testID = "TD004", - util = require("util"), - debug = require("debug")("apigeelint:" + testID), - Bundle = require("../../lib/package/Bundle.js"), - bl = require("../../lib/package/bundleLinter.js"), - Endpoint = require("../../lib/package/Endpoint.js"), - plugin = require(bl.resolvePlugin(testID)), - Dom = require("@xmldom/xmldom").DOMParser, - testBase = function (caseNum, profile, desc, targetDef, messages) { - it(`case ${caseNum} ${desc}`, function () { - const tDoc = new Dom().parseFromString(targetDef), - target = new Endpoint(tDoc.documentElement, this, ""); - - plugin.onBundle({ profile }); - - plugin.onTargetEndpoint(target, function (e, result) { - assert.equal(e, undefined, e ? " error " : " no error"); - debug(`result: ${result}`); - if (messages && messages.length) { - debug(`messages: ${util.format(messages)}`); - assert.equal(result, true); - assert.equal( - target.report.messages.length, - messages.length, - util.format(target.report.messages) - ); - messages.forEach((msg, ix) => { - debug(`check msg ${ix}: ${msg}`); - assert.ok( - target.report.messages.find((m) => m.message == msg), - `index ${ix} ${util.format(target.report.messages)}` - ); - }); - } else { - assert.equal(result, false, util.format(target.report.messages)); - assert.equal( - target.report.messages.length, - 0, - util.format(target.report.messages) - ); - } - }); - }); - }; - -const test = function (caseNum, desc, targetDef, messages) { - return testBase(caseNum, "apigee", desc, targetDef, messages); -}; -const testApigeeX = function (caseNum, desc, targetDef, messages) { - return testBase(caseNum, "apigeex", desc, targetDef, messages); -}; - -describe(`${testID} - ${plugin.plugin.name}`, function () { - test( - 10, - "missing SSLInfo", - ` - - https://foo.com/apis/{api_name}/maskconfigs - - `, - ["Missing SSLInfo configuration"] - ); - - test( - 20, - "empty SSLInfo with https url", - ` - - - https://foo.com/apis/{api_name}/maskconfigs - - `, - ["SSLInfo configuration does not use Enabled=true"] - ); - test( - 21, - "empty SSLInfo with http url", - ` - - - http://foo.com/apis/{api_name}/maskconfigs - - `, - ["SSLInfo should not be used with an insecure http url"] - ); - - test( - 22, - "non-empty SSLInfo with http url", - ` - - - false - - http://foo.com/apis/{api_name}/maskconfigs - - `, - ["SSLInfo should not be used with an insecure http url"] - ); - - test( - 23, - "no SSLInfo, using http url", - ` - - http://foo.com/apis/{api_name}/maskconfigs - - `, - null /* http URL does not require SSLInfo */ - ); - - test( - 30, - "SSLInfo Enabled = false", - ` - - - false - - https://foo.com/apis/{api_name}/maskconfigs - - `, - ["SSLInfo configuration does not use Enabled=true"] - ); - - test( - 40, - "SSLInfo Enabled = true, no truststore", - ` - - - true - - https://foo.com/apis/{api_name}/maskconfigs - - `, - [] - ); - - test( - 41, - "SSLInfo Enabled = true, with TrustStore", - ` - - - true - truststore1 - - https://foo.com/apis/{api_name}/maskconfigs - - `, - null - ); - - test( - 42, - "SSLInfo Enabled = true, ignoreerrors = false", - ` - - - true - false - truststore1 - - https://foo.com/apis/{api_name}/maskconfigs - - `, - null - ); - - test( - 43, - "SSLInfo Enabled = true, with LoadBalancer", - ` - - - true - false - truststore1 - - - - - - - `, - [] - ); - - test( - 50, - "SSLInfo IgnoreValidationErrors = true", - ` - - - true - true - - https://foo.com/apis/{api_name}/maskconfigs - - `, - ["SSLInfo configuration includes IgnoreValidationErrors = true"] - ); - - test( - 60, - "SSLInfo IgnoreValidationErrors = true and Enabled = false", - ` - - - false - true - truststore1 - - https://foo.com/apis/{api_name}/maskconfigs - - `, - [ - "SSLInfo configuration includes IgnoreValidationErrors = true", - "SSLInfo configuration does not use Enabled=true" - ] - ); - - test( - 70, - "SSLInfo ClientAuthEnabled all good", - ` - - - true - true - truststore1 - keystore1 - key1 - - https://foo.com/apis/{api_name}/maskconfigs - - `, - null - ); - - test( - 71, - "SSLInfo ClientAuthEnabled missing KeyStore + KeyAlias", - ` - - - true - true - truststore1 - - - https://foo.com/apis/{api_name}/maskconfigs - - `, - ["When ClientAuthEnabled = true, use a KeyStore and KeyAlias"] - ); - - test( - 72, - "SSLInfo ClientAuthEnabled missing KeyAlias", - ` - - - true - true - truststore1 - keystore1 - - - https://foo.com/apis/{api_name}/maskconfigs - - `, - ["When ClientAuthEnabled = true, use a KeyStore and KeyAlias"] - ); - - test( - 73, - "SSLInfo ClientAuthEnabled = false, includes KeyStore + KeyAlias", - ` - - - true - false - truststore1 - keystore1 - key1 - - https://foo.com/apis/{api_name}/maskconfigs - - `, - ["When ClientAuthEnabled = false, do not use a KeyStore and KeyAlias"] - ); - - test( - 74, - "SSLInfo ClientAuthEnabled element not present, includes KeyStore + KeyAlias", - ` - - - true - - truststore1 - keystore1 - key1 - - https://foo.com/apis/{api_name}/maskconfigs - - `, - ["When ClientAuthEnabled = false, do not use a KeyStore and KeyAlias"] - ); - - test( - 80, - "SSLInfo with URL and LoadBalancer", - ` - - - true - truststore1 - - https://foo.com/apis/{api_name}/maskconfigs - - - - - - `, - ["Using both URL and LoadBalancer in a proxy leads to undefined behavior"] - ); - - testApigeeX( - 90, - "SSLInfo with Enforce", - ` - - - true - true - truststore1 - - https://foo.com/apis/{api_name}/maskconfigs - - `, - null - ); - - testApigeeX( - 91, - "SSLInfo with Enforce", - ` - - - true - truststore1 - - https://foo.com/apis/{api_name}/maskconfigs - - `, - ["SSLInfo configuration does not use Enforce=true"] - ); - - test( - 92, - "SSLInfo with Enforce - not ApigeeX", - ` - - - true - true - truststore1 - - https://foo.com/apis/{api_name}/maskconfigs - - `, - ["SSLInfo configuration must not use the Enforce element"] - ); -}); - -describe(`${testID} - Print plugin results`, function () { - debug("test configuration: " + JSON.stringify(configuration)); - var bundle = new Bundle(configuration); - bl.executePlugin(testID, bundle); - let report = bundle.getReport(); - - it("should create a report object with valid schema", function () { - let formatter = bl.getFormatter("json.js"); - if (!formatter) { - assert.fail("formatter implementation not defined"); - } - let schema = require("./../fixtures/reportSchema.js"), - Validator = require("jsonschema").Validator, - v = new Validator(), - jsonReport = JSON.parse(formatter(report)), - validationResult = v.validate(jsonReport, schema); - assert.equal(validationResult.errors.length, 0, validationResult.errors); - }); -}); diff --git a/test/specs/TD004-test-sslInfo.js b/test/specs/TD004-test-sslInfo.js new file mode 100644 index 0000000..14f9d7f --- /dev/null +++ b/test/specs/TD004-test-sslInfo.js @@ -0,0 +1,212 @@ +/* + Copyright 2019-2024 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. +*/ +/* global describe, it, configuration */ + +const assert = require("assert"), + testID = "TD004", + util = require("util"), + debug = require("debug")("apigeelint:" + testID), + Bundle = require("../../lib/package/Bundle.js"), + bl = require("../../lib/package/bundleLinter.js"), + Endpoint = require("../../lib/package/Endpoint.js"), + plugin = require(bl.resolvePlugin(testID)), + Dom = require("@xmldom/xmldom").DOMParser, + testBase = function (caseNum, profile, desc, targetDef, messages) { + it(`case ${caseNum} ${desc}`, function () { + const tDoc = new Dom().parseFromString(targetDef), + target = new Endpoint(tDoc.documentElement, this, ""); + + plugin.onBundle({ profile }); + + plugin.onTargetEndpoint(target, function (e, result) { + assert.equal(e, undefined, e ? " error " : " no error"); + debug(`result: ${result}`); + if (messages && messages.length) { + debug(`messages: ${util.format(messages)}`); + assert.equal(result, true); + assert.equal( + target.report.messages.length, + messages.length, + util.format(target.report.messages) + ); + messages.forEach((msg, ix) => { + debug(`check msg ${ix}: ${msg}`); + assert.ok( + target.report.messages.find((m) => m.message == msg), + `index ${ix} ${util.format(target.report.messages)}` + ); + }); + } else { + assert.equal(result, false, util.format(target.report.messages)); + assert.equal( + target.report.messages.length, + 0, + util.format(target.report.messages) + ); + } + }); + }); + }; + +const test = function (caseNum, desc, targetDef, messages) { + return testBase(caseNum, "apigee", desc, targetDef, messages); +}; +const testApigeeX = function (caseNum, desc, targetDef, messages) { + return testBase(caseNum, "apigeex", desc, targetDef, messages); +}; + +describe(`${testID} - ${plugin.plugin.name}`, function () { + test( + 20, + "empty SSLInfo with https url", + ` + + + https://foo.com/apis/{api_name}/maskconfigs + + `, + ["SSLInfo configuration does not use Enabled=true"] + ); + + test( + 30, + "SSLInfo Enabled = false", + ` + + + false + + https://foo.com/apis/{api_name}/maskconfigs + + `, + ["SSLInfo configuration does not use Enabled=true"] + ); + + test( + 40, + "SSLInfo Enabled = true, no truststore", + ` + + + true + + https://foo.com/apis/{api_name}/maskconfigs + + `, + [] + ); + + test( + 41, + "SSLInfo Enabled = true, with TrustStore", + ` + + + true + truststore1 + + https://foo.com/apis/{api_name}/maskconfigs + + `, + null + ); + + // test( + // 43, + // "SSLInfo Enabled = true, with LoadBalancer", + // ` + // + // + // true + // false + // truststore1 + // + // + // + // + // + // + // `, + // [] + // ); + + testApigeeX( + 90, + "SSLInfo with Enforce", + ` + + + true + true + truststore1 + + https://foo.com/apis/{api_name}/maskconfigs + + `, + null + ); + + testApigeeX( + 91, + "SSLInfo with Enforce", + ` + + + true + truststore1 + + https://foo.com/apis/{api_name}/maskconfigs + + `, + ["SSLInfo configuration does not use Enforce=true"] + ); + + test( + 92, + "SSLInfo with Enforce - not ApigeeX", + ` + + + true + true + truststore1 + + https://foo.com/apis/{api_name}/maskconfigs + + `, + ["SSLInfo configuration must not use the Enforce element"] + ); +}); + +describe(`${testID} - Print plugin results`, function () { + debug("test configuration: " + JSON.stringify(configuration)); + var bundle = new Bundle(configuration); + bl.executePlugin(testID, bundle); + let report = bundle.getReport(); + + it("should create a report object with valid schema", function () { + let formatter = bl.getFormatter("json.js"); + if (!formatter) { + assert.fail("formatter implementation not defined"); + } + let schema = require("./../fixtures/reportSchema.js"), + Validator = require("jsonschema").Validator, + v = new Validator(), + jsonReport = JSON.parse(formatter(report)), + validationResult = v.validate(jsonReport, schema); + assert.equal(validationResult.errors.length, 0, validationResult.errors); + }); +}); diff --git a/test/specs/TD011-test.js b/test/specs/TD011-test.js new file mode 100644 index 0000000..ee186c6 --- /dev/null +++ b/test/specs/TD011-test.js @@ -0,0 +1,93 @@ +/* + Copyright 2019-2024 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. +*/ + +/* global describe, it */ + +const testID = "TD011", + assert = require("assert"), + fs = require("fs"), + util = require("util"), + path = require("path"), + bl = require("../../lib/package/bundleLinter.js"), + plugin = require(bl.resolvePlugin(testID)), + Endpoint = require("../../lib/package/Endpoint.js"), + Dom = require("@xmldom/xmldom").DOMParser, + rootDir = path.resolve(__dirname, "../fixtures/resources/TD011"), + debug = require("debug")("apigeelint:" + testID); + +const loadEndpoint = (sourceDir, shortFileName) => { + const fqPath = path.join(sourceDir, shortFileName), + xml = fs.readFileSync(fqPath).toString("utf-8"), + doc = new Dom().parseFromString(xml), + endpoint = new Endpoint(doc.documentElement, null, ""); + endpoint.getFileName = () => shortFileName; + return endpoint; +}; + +describe(`${testID} - endpoint passes SSLInfo IgnoreValidationErrors check`, function () { + const sourceDir = path.join(rootDir, "pass"); + const testOne = (shortFileName) => { + const endpoint = loadEndpoint(sourceDir, shortFileName); + + it(`check ${shortFileName} passes`, () => { + plugin.onTargetEndpoint(endpoint, (e, foundIssues) => { + assert.equal(e, undefined, "should be undefined"); + const messages = endpoint.getReport().messages; + debug(util.format(messages)); + assert.equal(foundIssues, false, "should be no issues"); + assert.ok(messages, "messages should exist"); + assert.equal(messages.length, 0, "unexpected number of messages"); + }); + }); + }; + + const candidates = fs + .readdirSync(sourceDir) + .filter((shortFileName) => shortFileName.endsWith(".xml")); + + it(`checks that there are tests`, () => { + assert.ok(candidates.length > 1, "tests should exist"); + }); + + candidates.forEach(testOne); +}); + +describe(`${testID} - endpoint does not pass SSLInfo IgnoreValidationErrors check`, () => { + const sourceDir = path.join(rootDir, "fail"); + + const testOne = (shortFileName) => { + const policy = loadEndpoint(sourceDir, shortFileName); + it(`check ${shortFileName} throws error`, () => { + plugin.onTargetEndpoint(policy, (e, foundIssues) => { + assert.equal(undefined, e, "should be undefined"); + assert.equal(true, foundIssues, "should be issues"); + const messages = policy.getReport().messages; + assert.ok(messages, "messages for issues should exist"); + debug(util.format(messages)); + }); + }); + }; + + const candidates = fs + .readdirSync(sourceDir) + .filter((shortFileName) => shortFileName.endsWith(".xml")); + + it(`checks that there are tests`, () => { + assert.ok(candidates.length > 0, "tests should exist"); + }); + + candidates.forEach(testOne); +}); diff --git a/test/specs/TD012-test.js b/test/specs/TD012-test.js new file mode 100644 index 0000000..dc9fb5d --- /dev/null +++ b/test/specs/TD012-test.js @@ -0,0 +1,93 @@ +/* + Copyright 2019-2024 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. +*/ + +/* global describe, it */ + +const testID = "TD012", + assert = require("assert"), + fs = require("fs"), + util = require("util"), + path = require("path"), + bl = require("../../lib/package/bundleLinter.js"), + plugin = require(bl.resolvePlugin(testID)), + Endpoint = require("../../lib/package/Endpoint.js"), + Dom = require("@xmldom/xmldom").DOMParser, + rootDir = path.resolve(__dirname, "../fixtures/resources/TD012"), + debug = require("debug")("apigeelint:" + testID); + +const loadEndpoint = (sourceDir, shortFileName) => { + const fqPath = path.join(sourceDir, shortFileName), + xml = fs.readFileSync(fqPath).toString("utf-8"), + doc = new Dom().parseFromString(xml), + endpoint = new Endpoint(doc.documentElement, null, ""); + endpoint.getFileName = () => shortFileName; + return endpoint; +}; + +describe(`${testID} - endpoint passes multiple SSLInfo check`, function () { + const sourceDir = path.join(rootDir, "pass"); + const testOne = (shortFileName) => { + const endpoint = loadEndpoint(sourceDir, shortFileName); + + it(`check ${shortFileName} passes`, () => { + plugin.onTargetEndpoint(endpoint, (e, foundIssues) => { + assert.equal(e, undefined, "should be undefined"); + const messages = endpoint.getReport().messages; + debug(util.format(messages)); + assert.equal(foundIssues, false, "should be no issues"); + assert.ok(messages, "messages should exist"); + assert.equal(messages.length, 0, "unexpected number of messages"); + }); + }); + }; + + const candidates = fs + .readdirSync(sourceDir) + .filter((shortFileName) => shortFileName.endsWith(".xml")); + + it(`checks that there are tests`, () => { + assert.ok(candidates.length > 0, "tests should exist"); + }); + + candidates.forEach(testOne); +}); + +describe(`${testID} - endpoint does not pass multiple SSLInfo check`, () => { + const sourceDir = path.join(rootDir, "fail"); + + const testOne = (shortFileName) => { + const policy = loadEndpoint(sourceDir, shortFileName); + it(`check ${shortFileName} throws error`, () => { + plugin.onTargetEndpoint(policy, (e, foundIssues) => { + assert.equal(undefined, e, "should be undefined"); + assert.equal(true, foundIssues, "should be issues"); + const messages = policy.getReport().messages; + assert.ok(messages, "messages for issues should exist"); + debug(util.format(messages)); + }); + }); + }; + + const candidates = fs + .readdirSync(sourceDir) + .filter((shortFileName) => shortFileName.endsWith(".xml")); + + it(`checks that there are tests`, () => { + assert.ok(candidates.length > 0, "tests should exist"); + }); + + candidates.forEach(testOne); +}); diff --git a/test/specs/TD013-test.js b/test/specs/TD013-test.js new file mode 100644 index 0000000..68d0e51 --- /dev/null +++ b/test/specs/TD013-test.js @@ -0,0 +1,93 @@ +/* + Copyright 2019-2024 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. +*/ + +/* global describe, it */ + +const testID = "TD013", + assert = require("assert"), + fs = require("fs"), + util = require("util"), + path = require("path"), + bl = require("../../lib/package/bundleLinter.js"), + plugin = require(bl.resolvePlugin(testID)), + Endpoint = require("../../lib/package/Endpoint.js"), + Dom = require("@xmldom/xmldom").DOMParser, + rootDir = path.resolve(__dirname, "../fixtures/resources/TD013"), + debug = require("debug")("apigeelint:" + testID); + +const loadEndpoint = (sourceDir, shortFileName) => { + const fqPath = path.join(sourceDir, shortFileName), + xml = fs.readFileSync(fqPath).toString("utf-8"), + doc = new Dom().parseFromString(xml), + endpoint = new Endpoint(doc.documentElement, null, ""); + endpoint.getFileName = () => shortFileName; + return endpoint; +}; + +describe(`${testID} - endpoint passes ClientAuthEnabled check`, function () { + const sourceDir = path.join(rootDir, "pass"); + const testOne = (shortFileName) => { + const endpoint = loadEndpoint(sourceDir, shortFileName); + + it(`check ${shortFileName} passes`, () => { + plugin.onTargetEndpoint(endpoint, (e, foundIssues) => { + assert.equal(e, undefined, "should be undefined"); + const messages = endpoint.getReport().messages; + debug(util.format(messages)); + assert.equal(foundIssues, false, "should be no issues"); + assert.ok(messages, "messages should exist"); + assert.equal(messages.length, 0, "unexpected number of messages"); + }); + }); + }; + + const candidates = fs + .readdirSync(sourceDir) + .filter((shortFileName) => shortFileName.endsWith(".xml")); + + it(`checks that there are tests`, () => { + assert.ok(candidates.length > 0, "tests should exist"); + }); + + candidates.forEach(testOne); +}); + +describe(`${testID} - endpoint does not pass ClientAuthEnabled check`, () => { + const sourceDir = path.join(rootDir, "fail"); + + const testOne = (shortFileName) => { + const policy = loadEndpoint(sourceDir, shortFileName); + it(`check ${shortFileName} throws error`, () => { + plugin.onTargetEndpoint(policy, (e, foundIssues) => { + assert.equal(undefined, e, "should be undefined"); + assert.equal(true, foundIssues, "should be issues"); + const messages = policy.getReport().messages; + assert.ok(messages, "messages for issues should exist"); + debug(util.format(messages)); + }); + }); + }; + + const candidates = fs + .readdirSync(sourceDir) + .filter((shortFileName) => shortFileName.endsWith(".xml")); + + it(`checks that there are tests`, () => { + assert.ok(candidates.length > 0, "tests should exist"); + }); + + candidates.forEach(testOne); +}); diff --git a/test/specs/TD014-test.js b/test/specs/TD014-test.js new file mode 100644 index 0000000..ff0bd59 --- /dev/null +++ b/test/specs/TD014-test.js @@ -0,0 +1,93 @@ +/* + Copyright 2019-2024 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. +*/ + +/* global describe, it */ + +const testID = "TD014", + assert = require("assert"), + fs = require("fs"), + util = require("util"), + path = require("path"), + bl = require("../../lib/package/bundleLinter.js"), + plugin = require(bl.resolvePlugin(testID)), + Endpoint = require("../../lib/package/Endpoint.js"), + Dom = require("@xmldom/xmldom").DOMParser, + rootDir = path.resolve(__dirname, "../fixtures/resources/TD014"), + debug = require("debug")("apigeelint:" + testID); + +const loadEndpoint = (sourceDir, shortFileName) => { + const fqPath = path.join(sourceDir, shortFileName), + xml = fs.readFileSync(fqPath).toString("utf-8"), + doc = new Dom().parseFromString(xml), + endpoint = new Endpoint(doc.documentElement, null, ""); + endpoint.getFileName = () => shortFileName; + return endpoint; +}; + +describe(`${testID} - endpoint passes exactly one URL or LoadBalancer check`, function () { + const sourceDir = path.join(rootDir, "pass"); + const testOne = (shortFileName) => { + const endpoint = loadEndpoint(sourceDir, shortFileName); + + it(`check ${shortFileName} passes`, () => { + plugin.onTargetEndpoint(endpoint, (e, foundIssues) => { + assert.equal(e, undefined, "should be undefined"); + const messages = endpoint.getReport().messages; + debug(util.format(messages)); + assert.equal(foundIssues, false, "should be no issues"); + assert.ok(messages, "messages should exist"); + assert.equal(messages.length, 0, "unexpected number of messages"); + }); + }); + }; + + const candidates = fs + .readdirSync(sourceDir) + .filter((shortFileName) => shortFileName.endsWith(".xml")); + + it(`checks that there are tests`, () => { + assert.ok(candidates.length > 0, "tests should exist"); + }); + + candidates.forEach(testOne); +}); + +describe(`${testID} - endpoint does not pass exactly one URL or LoadBalancer check`, () => { + const sourceDir = path.join(rootDir, "fail"); + + const testOne = (shortFileName) => { + const policy = loadEndpoint(sourceDir, shortFileName); + it(`check ${shortFileName} throws error`, () => { + plugin.onTargetEndpoint(policy, (e, foundIssues) => { + assert.equal(undefined, e, "should be undefined"); + assert.equal(true, foundIssues, "should be issues"); + const messages = policy.getReport().messages; + assert.ok(messages, "messages for issues should exist"); + debug(util.format(messages)); + }); + }); + }; + + const candidates = fs + .readdirSync(sourceDir) + .filter((shortFileName) => shortFileName.endsWith(".xml")); + + it(`checks that there are tests`, () => { + assert.ok(candidates.length > 0, "tests should exist"); + }); + + candidates.forEach(testOne); +}); From 08eb136b355dd69d6f809e3ae553a0e0278ab99f Mon Sep 17 00:00:00 2001 From: Dino Chiesa Date: Fri, 27 Sep 2024 16:25:27 -0700 Subject: [PATCH 2/4] add test for two SSLInfo wth LoadBalancer --- ...TD012-targetSslInfo-exactly-one-SSLInfo.js | 8 +++++--- ...Infos.xml => t1-two-SSLInfos-with-URL.xml} | 0 .../t4-two-SSLInfos-with-LoadBalancer.xml | 15 +++++++++++++++ ...SSLInfo.xml => t1-one-SSLInfo-and-URL.xml} | 0 .../pass/t3-SSLInfo-with-LoadBalancer.xml | 14 ++++++++++++++ test/specs/TD004-test-sslInfo.js | 19 ------------------- 6 files changed, 34 insertions(+), 22 deletions(-) rename test/fixtures/resources/TD012/fail/{t1-two-SSLInfos.xml => t1-two-SSLInfos-with-URL.xml} (100%) create mode 100644 test/fixtures/resources/TD012/fail/t4-two-SSLInfos-with-LoadBalancer.xml rename test/fixtures/resources/TD012/pass/{t1-one-SSLInfo.xml => t1-one-SSLInfo-and-URL.xml} (100%) create mode 100644 test/fixtures/resources/TD012/pass/t3-SSLInfo-with-LoadBalancer.xml diff --git a/lib/package/plugins/TD012-targetSslInfo-exactly-one-SSLInfo.js b/lib/package/plugins/TD012-targetSslInfo-exactly-one-SSLInfo.js index 952d3bf..5f80b7b 100644 --- a/lib/package/plugins/TD012-targetSslInfo-exactly-one-SSLInfo.js +++ b/lib/package/plugins/TD012-targetSslInfo-exactly-one-SSLInfo.js @@ -40,14 +40,16 @@ const onTargetEndpoint = function (endpoint, cb) { if (htc) { try { const urls = htc.select("URL"); - if (urls.length == 1) { + const loadBalancers = htc.select("LoadBalancer"); + if (urls.length == 1 || loadBalancers.length == 1) { const sslInfos = htc.select("SSLInfo"); const endpointUrl = + urls[0] && urls[0].childNodes && urls[0].childNodes[0] && urls[0].childNodes[0].nodeValue; - const isHttps = endpointUrl.startsWith("https://"); - if (isHttps) { + const isHttps = endpointUrl && endpointUrl.startsWith("https://"); + if (isHttps || loadBalancers.length == 1) { debug( `onTargetEndpoint sslInfos.length(${util.format(sslInfos.length)})` ); diff --git a/test/fixtures/resources/TD012/fail/t1-two-SSLInfos.xml b/test/fixtures/resources/TD012/fail/t1-two-SSLInfos-with-URL.xml similarity index 100% rename from test/fixtures/resources/TD012/fail/t1-two-SSLInfos.xml rename to test/fixtures/resources/TD012/fail/t1-two-SSLInfos-with-URL.xml diff --git a/test/fixtures/resources/TD012/fail/t4-two-SSLInfos-with-LoadBalancer.xml b/test/fixtures/resources/TD012/fail/t4-two-SSLInfos-with-LoadBalancer.xml new file mode 100644 index 0000000..ee6f378 --- /dev/null +++ b/test/fixtures/resources/TD012/fail/t4-two-SSLInfos-with-LoadBalancer.xml @@ -0,0 +1,15 @@ + + + + true + true + truststore1 + + + + + + + + + diff --git a/test/fixtures/resources/TD012/pass/t1-one-SSLInfo.xml b/test/fixtures/resources/TD012/pass/t1-one-SSLInfo-and-URL.xml similarity index 100% rename from test/fixtures/resources/TD012/pass/t1-one-SSLInfo.xml rename to test/fixtures/resources/TD012/pass/t1-one-SSLInfo-and-URL.xml diff --git a/test/fixtures/resources/TD012/pass/t3-SSLInfo-with-LoadBalancer.xml b/test/fixtures/resources/TD012/pass/t3-SSLInfo-with-LoadBalancer.xml new file mode 100644 index 0000000..24fd2cb --- /dev/null +++ b/test/fixtures/resources/TD012/pass/t3-SSLInfo-with-LoadBalancer.xml @@ -0,0 +1,14 @@ + + + + + true + false + truststore1 + + + + + + + diff --git a/test/specs/TD004-test-sslInfo.js b/test/specs/TD004-test-sslInfo.js index 14f9d7f..0e2240f 100644 --- a/test/specs/TD004-test-sslInfo.js +++ b/test/specs/TD004-test-sslInfo.js @@ -124,25 +124,6 @@ describe(`${testID} - ${plugin.plugin.name}`, function () { null ); - // test( - // 43, - // "SSLInfo Enabled = true, with LoadBalancer", - // ` - // - // - // true - // false - // truststore1 - // - // - // - // - // - // - // `, - // [] - // ); - testApigeeX( 90, "SSLInfo with Enforce", From d461c43296d4272827c4ff14d17ec6ef24e4cc86 Mon Sep 17 00:00:00 2001 From: Dino Chiesa Date: Fri, 27 Sep 2024 16:32:31 -0700 Subject: [PATCH 3/4] fixup EP002 test to account for new TD012 error --- test/specs/EP002-elementPlacement.js | 155 ---------------------- test/specs/EP002-test.js | 184 +++++++++++++++++++++++++++ 2 files changed, 184 insertions(+), 155 deletions(-) delete mode 100644 test/specs/EP002-elementPlacement.js create mode 100644 test/specs/EP002-test.js diff --git a/test/specs/EP002-elementPlacement.js b/test/specs/EP002-elementPlacement.js deleted file mode 100644 index fb07b91..0000000 --- a/test/specs/EP002-elementPlacement.js +++ /dev/null @@ -1,155 +0,0 @@ -/* - Copyright 2019-2022 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. -*/ - -/* global describe, it */ - -const assert = require("assert"), - path = require("path"), - util = require('util'), - bl = require("../../lib/package/bundleLinter.js"); - -describe(`EP002 - apiproxy bundle with misplaced elements`, () => { - - let configuration = { - debug: true, - source: { - type: "filesystem", - path: path.resolve(__dirname, '../fixtures/resources/EP002/apiproxy'), - bundleType: "apiproxy" - }, - profile: 'apigeex', - excluded: {}, - setExitCode: false, - output: () => {} // suppress output - }; - - bl.lint(configuration, (bundle) => { - let items = bundle.getReport(); - let ep002Errors = items.filter(item => item.messages && item.messages.length && - item.messages.find(m => m.ruleId == 'EP002')); - - it('should generate the expected errors', () => { - assert.ok(items); - assert.ok(items.length); - assert.equal(ep002Errors.length, 2); - }); - - it('should generate the correct messages for proxy endpoint 1', () => { - let proxyEp1Errors = ep002Errors.filter( item => item.filePath.endsWith('/apiproxy/proxies/proxy-endpoint-1.xml')); - assert.ok(proxyEp1Errors); - assert.equal(proxyEp1Errors.length, 1); - let expectedErrors = [ - 'Extra FaultRules element', - 'Invalid Framjo element', - 'Misplaced DefaultFaultRule element child of Framjo', - 'Misplaced Step element child of PostClientFlow', - 'Invalid Flow element' - ]; - assert.equal(proxyEp1Errors[0].messages.length, expectedErrors.length, "number of errors"); - proxyEp1Errors[0].messages.forEach( msg => { - assert.ok(msg.message); - assert.ok(expectedErrors.includes(msg.message)); - // disallow repeats - expectedErrors = expectedErrors.filter( item => item != msg.message); - }); - assert.equal(expectedErrors.length, 0); - }); - - it('should generate the correct messages for proxy endpoint 2', () => { - let proxyEp2Errors = ep002Errors.filter( item => item.filePath.endsWith('/apiproxy/proxies/proxy-endpoint-2.xml')); - assert.ok(proxyEp2Errors); - assert.equal(proxyEp2Errors.length, 0); - // assert.equal(proxyEp2Errors[0].messages.length, expectedErrors.length, "number of errors"); - // proxyEp2Errors[0].messages.forEach( msg => { - // assert.ok(msg.message); - // assert.ok(expectedErrors.includes(msg.message)); - // // disallow repeats - // expectedErrors = expectedErrors.filter( item => item != msg.message); - // }); - // assert.equal(expectedErrors.length, 0); - }); - - it('should generate the correct messages for the target endpoint', () => { - let targetErrors = ep002Errors.filter( item => item.filePath.endsWith('/apiproxy/targets/http-1.xml')); - assert.ok(targetErrors); - assert.equal(targetErrors.length, 1); - // console.log(util.format(targetErrors[0].messages)); - let expectedErrors = [ - 'Extra Flows element', - "Misplaced 'SocketReadTimeoutInSec' element child of Request", - "Misplaced 'HTTPMonitor' element child of HTTPTargetConnection", - "Misplaced 'ThisIsBogus' element child of HealthMonitor", - 'Invalid MisPlaced element', - 'LocalTargetConnection element conflicts with HTTPTargetConnection on line 26', - 'Invalid RouteRule element', - "Misplaced 'ConnectTimeoutInMin' element child of Request", - "Misplaced 'Status' element child of SuccessResponse", - 'Redundant HealthMonitor element', - 'TCPMonitor element conflicts with HTTPMonitor on line 47' - ]; - assert.equal(targetErrors[0].messages.length, expectedErrors.length, "number of errors"); - targetErrors[0].messages.forEach( msg => { - assert.ok(msg.message); - assert.ok(expectedErrors.includes(msg.message), msg.message); - // disallow repeats - expectedErrors = expectedErrors.filter( item => item != msg.message); - }); - }); - - it('should generate the correct messages for the target endpoint with URL', () => { - let targetErrors = ep002Errors.filter( item => item.filePath.endsWith('/apiproxy/targets/http-2.xml')); - assert.ok(targetErrors); - assert.equal(targetErrors.length, 0); - }); - - }); -}); - -describe(`EP002 - sharedflowbundle with no misplaced elements`, () => { - - let configuration = { - debug: true, - source: { - type: "filesystem", - path: path.resolve(__dirname, '../fixtures/resources/ExtractVariables-Attachment/sharedflowbundle'), - bundleType: "sharedflowbundle" - }, - profile: 'apigeex', - excluded: {}, - setExitCode: false, - output: () => {} // suppress output - }; - - bl.lint(configuration, (bundle) => { - let items = bundle.getReport(); - - it('should generate some errors', () => { - assert.ok(items); - assert.ok(items.length); - let itemsWithErrors = items.filter(item => item.messages && item.messages.length); - assert.equal(itemsWithErrors.length, 1); - }); - - it('should generate no EP002 errors', () => { - let ep002Errors = items.filter(item => item.messages && item.messages.length && - item.messages.find(m => m.ruleId == 'EP002')); - - assert.equal(ep002Errors.length, 0); - }); - - }); - -}); diff --git a/test/specs/EP002-test.js b/test/specs/EP002-test.js new file mode 100644 index 0000000..065aa2b --- /dev/null +++ b/test/specs/EP002-test.js @@ -0,0 +1,184 @@ +/* + Copyright 2019-2022 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. +*/ + +/* global describe, it */ + +const assert = require("assert"), + path = require("path"), + util = require("util"), + debug = require("debug")("apigeelint:EP002"), + bl = require("../../lib/package/bundleLinter.js"); + +describe(`EP002 - apiproxy bundle with misplaced elements`, () => { + let configuration = { + debug: true, + source: { + type: "filesystem", + path: path.resolve(__dirname, "../fixtures/resources/EP002/apiproxy"), + bundleType: "apiproxy" + }, + profile: "apigeex", + excluded: {}, + setExitCode: false, + output: () => {} // suppress output + }; + + bl.lint(configuration, (bundle) => { + let items = bundle.getReport(); + let ep002Errors = items.filter( + (item) => + item.messages && + item.messages.length && + item.messages.find((m) => m.ruleId == "EP002") + ); + + it("should generate the expected errors", () => { + assert.ok(items); + assert.ok(items.length); + assert.equal(ep002Errors.length, 2); + }); + + it("should generate the correct messages for proxy endpoint 1", () => { + let proxyEp1Errors = ep002Errors.filter((item) => + item.filePath.endsWith("/apiproxy/proxies/proxy-endpoint-1.xml") + ); + assert.ok(proxyEp1Errors); + assert.equal(proxyEp1Errors.length, 1); + let expectedErrors = [ + "Extra FaultRules element", + "Invalid Framjo element", + "Misplaced DefaultFaultRule element child of Framjo", + "Misplaced Step element child of PostClientFlow", + "Invalid Flow element" + ]; + assert.equal( + proxyEp1Errors[0].messages.length, + expectedErrors.length, + "number of errors" + ); + proxyEp1Errors[0].messages.forEach((msg) => { + assert.ok(msg.message); + assert.ok(expectedErrors.includes(msg.message)); + // disallow repeats + expectedErrors = expectedErrors.filter((item) => item != msg.message); + }); + assert.equal(expectedErrors.length, 0); + }); + + it("should generate the correct messages for proxy endpoint 2", () => { + let proxyEp2Errors = ep002Errors.filter((item) => + item.filePath.endsWith("/apiproxy/proxies/proxy-endpoint-2.xml") + ); + assert.ok(proxyEp2Errors); + assert.equal(proxyEp2Errors.length, 0); + // assert.equal(proxyEp2Errors[0].messages.length, expectedErrors.length, "number of errors"); + // proxyEp2Errors[0].messages.forEach( msg => { + // assert.ok(msg.message); + // assert.ok(expectedErrors.includes(msg.message)); + // // disallow repeats + // expectedErrors = expectedErrors.filter( item => item != msg.message); + // }); + // assert.equal(expectedErrors.length, 0); + }); + + it("should generate the correct messages for the target endpoint", () => { + let targetErrors = ep002Errors.filter((item) => + item.filePath.endsWith("/apiproxy/targets/http-1.xml") + ); + assert.ok(targetErrors); + assert.equal(targetErrors.length, 1); + // console.log(util.format(targetErrors[0].messages)); + let expectedErrors = [ + "Extra Flows element", + "Misplaced 'SocketReadTimeoutInSec' element child of Request", + "Misplaced 'HTTPMonitor' element child of HTTPTargetConnection", + "Misplaced 'ThisIsBogus' element child of HealthMonitor", + "Invalid MisPlaced element", + "LocalTargetConnection element conflicts with HTTPTargetConnection on line 26", + "Invalid RouteRule element", + "Misplaced 'ConnectTimeoutInMin' element child of Request", + "Misplaced 'Status' element child of SuccessResponse", + "Redundant HealthMonitor element", + "TCPMonitor element conflicts with HTTPMonitor on line 47" + ]; + debug(targetErrors[0].messages); + const ep002Messages = targetErrors[0].messages.filter( + (m) => m.ruleId == "EP002" + ); + assert.equal( + ep002Messages.length, + expectedErrors.length, + "number of errors" + ); + ep002Messages.forEach((msg) => { + assert.ok(msg.message); + assert.ok(expectedErrors.includes(msg.message), msg.message); + // disallow repeats + expectedErrors = expectedErrors.filter((item) => item != msg.message); + }); + }); + + it("should generate the correct messages for the target endpoint with URL", () => { + let targetErrors = ep002Errors.filter((item) => + item.filePath.endsWith("/apiproxy/targets/http-2.xml") + ); + assert.ok(targetErrors); + assert.equal(targetErrors.length, 0); + }); + }); +}); + +describe(`EP002 - sharedflowbundle with no misplaced elements`, () => { + let configuration = { + debug: true, + source: { + type: "filesystem", + path: path.resolve( + __dirname, + "../fixtures/resources/ExtractVariables-Attachment/sharedflowbundle" + ), + bundleType: "sharedflowbundle" + }, + profile: "apigeex", + excluded: {}, + setExitCode: false, + output: () => {} // suppress output + }; + + bl.lint(configuration, (bundle) => { + const items = bundle.getReport(); + + it("should generate some errors", () => { + assert.ok(items); + assert.ok(items.length); + const itemsWithErrors = items.filter( + (item) => item.messages && item.messages.length + ); + assert.equal(itemsWithErrors.length, 1); + }); + + it("should generate no EP002 errors", () => { + const ep002Errors = items.filter( + (item) => + item.messages && + item.messages.length && + item.messages.find((m) => m.ruleId == "EP002") + ); + + assert.equal(ep002Errors.length, 0); + }); + }); +}); From 804fff12341eb33e596faa5c9b2e295b05ee65f5 Mon Sep 17 00:00:00 2001 From: Dino Chiesa Date: Mon, 7 Oct 2024 10:20:50 -0700 Subject: [PATCH 4/4] TD015 check for non-zero MaxFailures when LoadBalancer is present --- README.md | 1 + ...TD004-targetSslInfo-enabled-and-enforce.js | 44 ++++--- .../plugins/TD015-target-LB-no-MaxFailures.js | 124 ++++++++++++++++++ ...h-LoadBalancer-and-missing-MaxFailures.xml | 16 +++ ...adBalancer-and-non-integer-MaxFailures.xml | 16 +++ ...with-LoadBalancer-and-zero-MaxFailures.xml | 16 +++ ...dBalancer-and-leading-zero-MaxFailures.xml | 16 +++ ...ith-LoadBalancer-and-empty-MaxFailures.xml | 16 +++ ...th-LoadBalancer-and-simple-MaxFailures.xml | 16 +++ ...dBalancer-and-double-digit-MaxFailures.xml | 16 +++ test/specs/EP002-test.js | 2 +- test/specs/TD004-test-sslInfo.js | 53 ++++++-- test/specs/TD008-test.js | 2 +- test/specs/TD009-test.js | 2 +- test/specs/TD010-test.js | 2 +- test/specs/TD011-test.js | 2 +- test/specs/TD012-test.js | 2 +- test/specs/TD013-test.js | 2 +- test/specs/TD014-test.js | 2 +- test/specs/TD015-test.js | 93 +++++++++++++ 20 files changed, 408 insertions(+), 35 deletions(-) create mode 100644 lib/package/plugins/TD015-target-LB-no-MaxFailures.js create mode 100644 test/fixtures/resources/TD015/fail/t1-with-LoadBalancer-and-missing-MaxFailures.xml create mode 100644 test/fixtures/resources/TD015/fail/t2-with-LoadBalancer-and-non-integer-MaxFailures.xml create mode 100644 test/fixtures/resources/TD015/fail/t3-with-LoadBalancer-and-zero-MaxFailures.xml create mode 100644 test/fixtures/resources/TD015/fail/t4-with-LoadBalancer-and-leading-zero-MaxFailures.xml create mode 100644 test/fixtures/resources/TD015/fail/t5-with-LoadBalancer-and-empty-MaxFailures.xml create mode 100644 test/fixtures/resources/TD015/pass/t1-with-LoadBalancer-and-simple-MaxFailures.xml create mode 100644 test/fixtures/resources/TD015/pass/t2-with-LoadBalancer-and-double-digit-MaxFailures.xml create mode 100644 test/specs/TD015-test.js diff --git a/README.md b/README.md index 9789079..e2274bd 100644 --- a/README.md +++ b/README.md @@ -342,6 +342,7 @@ This is the current list: |   |:white_check_mark:| TD012 | TargetEndpoint SSLInfo | TargetEndpoint HTTPTargetConnection should have exactly one SSLInfo. | |   |:white_check_mark:| TD013 | TargetEndpoint SSLInfo | TargetEndpoint HTTPTargetConnection should correctly configure ClientAuthEnbled. | |   |:white_check_mark:| TD014 | TargetEndpoint SSLInfo | TargetEndpoint HTTPTargetConnection should use exctly one of URL, LoadBalancer. | +|   |:white_check_mark:| TD015 | TargetEndpoint LoadBalancer | if TargetEndpoint HTTPTargetConnection uses a LoadBalancer, it should specify MaxFailures. | | Flow |   |   |   |   | |   |:white_check_mark:| FL001 | Unconditional Flows | Only one unconditional flow will get executed. Error if more than one was detected. | | Step |   |   |   |   | diff --git a/lib/package/plugins/TD004-targetSslInfo-enabled-and-enforce.js b/lib/package/plugins/TD004-targetSslInfo-enabled-and-enforce.js index 15625a4..daa9e85 100644 --- a/lib/package/plugins/TD004-targetSslInfo-enabled-and-enforce.js +++ b/lib/package/plugins/TD004-targetSslInfo-enabled-and-enforce.js @@ -18,9 +18,9 @@ const ruleId = require("../myUtil.js").getRuleId(); const plugin = { ruleId, - name: "TargetEndpoint HTTPTargetConnection SSLInfo should use TrustStore", + name: "TargetEndpoint HTTPTargetConnection SSLInfo should use Enabled and Enforce correctly", message: - "TargetEndpoint HTTPTargetConnection should use TrustStore with SSLInfo.", + "TargetEndpoint HTTPTargetConnection SSLInfo should use Enabled and Enforce correctly.", fatal: false, severity: 1, // 1 = warn, 2 = error nodeType: "Endpoint", @@ -56,33 +56,36 @@ const onTargetEndpoint = function (endpoint, cb) { if (sslInfos.length == 1) { debug(`onTargetEndpoint sslInfos(${util.format(sslInfos)})`); const urls = htc.select("URL"); - if (urls.length == 1) { + if (urls.length >= 1) { debug(`onTargetEndpoint url(${util.format(urls[0])})`); - const endpointUrl = urls[0].childNodes && urls[0].childNodes[0] && urls[0].childNodes[0].nodeValue; const isHttps = endpointUrl.startsWith("https://"); + let elts = htc.select(`SSLInfo/Enabled`); + const enabled = + elts && + elts[0] && + elts[0].childNodes && + elts[0].childNodes[0] && + elts[0].childNodes[0].nodeValue == "true"; + elts = htc.select(`SSLInfo/Enforce`); + const enforce = + elts && + elts[0] && + elts[0].childNodes && + elts[0].childNodes[0] && + elts[0].childNodes[0].nodeValue == "true"; + if (isHttps) { - let elts = htc.select(`SSLInfo/Enabled`); - const enabled = - elts && - elts[0] && - elts[0].childNodes && - elts[0].childNodes[0] && - elts[0].childNodes[0].nodeValue == "true"; if (!enabled) { messages.push( "SSLInfo configuration does not use Enabled=true" ); } - elts = htc.select(`SSLInfo/Enforce`); - let enforce = - elts && elts[0] && elts[0].childNodes && elts[0].childNodes[0]; if (bundleProfile == "apigeex") { - enforce = enforce && enforce.nodeValue == "true"; if (!enforce) { messages.push( "SSLInfo configuration does not use Enforce=true" @@ -95,6 +98,17 @@ const onTargetEndpoint = function (endpoint, cb) { ); } } + } else { + if (enabled) { + messages.push( + "SSLInfo configuration must not use the Enabled=true with insecure URL" + ); + } + if (enforce) { + messages.push( + "SSLInfo configuration must not use the Enforce=true with insecure URL" + ); + } } } //debug(`onTargetEndpoint messages(${messages})`); diff --git a/lib/package/plugins/TD015-target-LB-no-MaxFailures.js b/lib/package/plugins/TD015-target-LB-no-MaxFailures.js new file mode 100644 index 0000000..cda6e02 --- /dev/null +++ b/lib/package/plugins/TD015-target-LB-no-MaxFailures.js @@ -0,0 +1,124 @@ +/* + Copyright 2019-2024 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. +*/ + +const ruleId = require("../myUtil.js").getRuleId(), + xpath = require("xpath"); + +const plugin = { + ruleId, + name: "TargetEndpoint HTTPTargetConnection LoadBalancer should specify MaxFailures.", + message: + "TargetEndpoint HTTPTargetConnection LoadBalancer should specify MaxFailures.", + fatal: false, + severity: 1, // 1 = warn, 2 = error + nodeType: "Endpoint", + enabled: true +}; + +const path = require("path"), + debug = require("debug")("apigeelint:" + ruleId); + +const onTargetEndpoint = function (endpoint, cb) { + const htc = endpoint.getHTTPTargetConnection(), + shortFilename = path.basename(endpoint.getFileName()); + let flagged = false; + + debug(`onTargetEndpoint shortfile(${shortFilename})`); + if (htc) { + try { + const loadBalancers = htc.select("LoadBalancer"); + debug(`loadBalancers(${loadBalancers.length})`); + + if (loadBalancers.length == 1) { + const loadBalancer = loadBalancers[0]; + const maxFailuresElts = xpath.select("MaxFailures", loadBalancer); + if (maxFailuresElts.length == 0) { + endpoint.addMessage({ + plugin, + line: loadBalancer.lineNumber, + column: loadBalancer.columnNumber, + message: "Missing MaxFailures element within LoadBalancer" + }); + flagged = true; + } else if (maxFailuresElts.length == 1) { + const maxFailures = maxFailuresElts[0]; + const maxFailStringValue = + maxFailures.childNodes && + maxFailures.childNodes[0] && + maxFailures.childNodes[0].nodeValue; + debug(`maxFailStringValue(${maxFailStringValue})`); + const maxFailValue = Number( + maxFailStringValue && maxFailStringValue.trim() + ); + const intReg = new RegExp("^[1-9][0-9]*$"); + if (!maxFailStringValue) { + endpoint.addMessage({ + plugin, + line: maxFailures.lineNumber, + column: maxFailures.columnNumber, + message: "MaxFailures element provides missing or empty TEXT" + }); + flagged = true; + } else if (!intReg.test(maxFailStringValue)) { + endpoint.addMessage({ + plugin, + line: maxFailures.lineNumber, + column: maxFailures.columnNumber, + message: "MaxFailures element should specify a positive integer" + }); + flagged = true; + } else if (isNaN(maxFailValue)) { + endpoint.addMessage({ + plugin, + line: maxFailures.lineNumber, + column: maxFailures.columnNumber, + message: "MaxFailures element specifies a non-number" + }); + flagged = true; + } + } else { + endpoint.addMessage({ + plugin, + line: loadBalancer.lineNumber, + column: loadBalancer.columnNumber, + message: "More than one MaxFailures element within LoadBalancer" + }); + flagged = true; + } + } + } catch (exc1) { + console.error("exception: " + exc1); + debug(`onTargetEndpoint exc(${exc1})`); + debug(`${exc1.stack}`); + endpoint.addMessage({ + plugin, + message: "Exception while processing: " + exc1 + }); + + flagged = true; + } + } + + if (typeof cb == "function") { + debug(`onTargetEndpoint isFlagged(${flagged})`); + cb(null, flagged); + } +}; + +module.exports = { + plugin, + onTargetEndpoint +}; diff --git a/test/fixtures/resources/TD015/fail/t1-with-LoadBalancer-and-missing-MaxFailures.xml b/test/fixtures/resources/TD015/fail/t1-with-LoadBalancer-and-missing-MaxFailures.xml new file mode 100644 index 0000000..3e2c8b7 --- /dev/null +++ b/test/fixtures/resources/TD015/fail/t1-with-LoadBalancer-and-missing-MaxFailures.xml @@ -0,0 +1,16 @@ + + + + true + false + truststore1 + + + + + + three + + + + diff --git a/test/fixtures/resources/TD015/fail/t2-with-LoadBalancer-and-non-integer-MaxFailures.xml b/test/fixtures/resources/TD015/fail/t2-with-LoadBalancer-and-non-integer-MaxFailures.xml new file mode 100644 index 0000000..3e2c8b7 --- /dev/null +++ b/test/fixtures/resources/TD015/fail/t2-with-LoadBalancer-and-non-integer-MaxFailures.xml @@ -0,0 +1,16 @@ + + + + true + false + truststore1 + + + + + + three + + + + diff --git a/test/fixtures/resources/TD015/fail/t3-with-LoadBalancer-and-zero-MaxFailures.xml b/test/fixtures/resources/TD015/fail/t3-with-LoadBalancer-and-zero-MaxFailures.xml new file mode 100644 index 0000000..04aa773 --- /dev/null +++ b/test/fixtures/resources/TD015/fail/t3-with-LoadBalancer-and-zero-MaxFailures.xml @@ -0,0 +1,16 @@ + + + + true + false + truststore1 + + + + + + 0 + + + + diff --git a/test/fixtures/resources/TD015/fail/t4-with-LoadBalancer-and-leading-zero-MaxFailures.xml b/test/fixtures/resources/TD015/fail/t4-with-LoadBalancer-and-leading-zero-MaxFailures.xml new file mode 100644 index 0000000..78036b4 --- /dev/null +++ b/test/fixtures/resources/TD015/fail/t4-with-LoadBalancer-and-leading-zero-MaxFailures.xml @@ -0,0 +1,16 @@ + + + + true + false + truststore1 + + + + + + 03 + + + + diff --git a/test/fixtures/resources/TD015/fail/t5-with-LoadBalancer-and-empty-MaxFailures.xml b/test/fixtures/resources/TD015/fail/t5-with-LoadBalancer-and-empty-MaxFailures.xml new file mode 100644 index 0000000..23e7d1a --- /dev/null +++ b/test/fixtures/resources/TD015/fail/t5-with-LoadBalancer-and-empty-MaxFailures.xml @@ -0,0 +1,16 @@ + + + + true + false + truststore1 + + + + + + + + + + diff --git a/test/fixtures/resources/TD015/pass/t1-with-LoadBalancer-and-simple-MaxFailures.xml b/test/fixtures/resources/TD015/pass/t1-with-LoadBalancer-and-simple-MaxFailures.xml new file mode 100644 index 0000000..35f65ed --- /dev/null +++ b/test/fixtures/resources/TD015/pass/t1-with-LoadBalancer-and-simple-MaxFailures.xml @@ -0,0 +1,16 @@ + + + + true + false + truststore1 + + + + + + 3 + + + + diff --git a/test/fixtures/resources/TD015/pass/t2-with-LoadBalancer-and-double-digit-MaxFailures.xml b/test/fixtures/resources/TD015/pass/t2-with-LoadBalancer-and-double-digit-MaxFailures.xml new file mode 100644 index 0000000..87b977b --- /dev/null +++ b/test/fixtures/resources/TD015/pass/t2-with-LoadBalancer-and-double-digit-MaxFailures.xml @@ -0,0 +1,16 @@ + + + + true + false + truststore1 + + + + + + 13 + + + + diff --git a/test/specs/EP002-test.js b/test/specs/EP002-test.js index 065aa2b..80d3704 100644 --- a/test/specs/EP002-test.js +++ b/test/specs/EP002-test.js @@ -19,7 +19,7 @@ const assert = require("assert"), path = require("path"), util = require("util"), - debug = require("debug")("apigeelint:EP002"), + debug = require("debug")("apigeelint:EP002-test"), bl = require("../../lib/package/bundleLinter.js"); describe(`EP002 - apiproxy bundle with misplaced elements`, () => { diff --git a/test/specs/TD004-test-sslInfo.js b/test/specs/TD004-test-sslInfo.js index 0e2240f..ea3c19d 100644 --- a/test/specs/TD004-test-sslInfo.js +++ b/test/specs/TD004-test-sslInfo.js @@ -18,7 +18,7 @@ const assert = require("assert"), testID = "TD004", util = require("util"), - debug = require("debug")("apigeelint:" + testID), + debug = require("debug")(`apigeelint:${testID}-test`), Bundle = require("../../lib/package/Bundle.js"), bl = require("../../lib/package/bundleLinter.js"), Endpoint = require("../../lib/package/Endpoint.js"), @@ -31,12 +31,12 @@ const assert = require("assert"), plugin.onBundle({ profile }); - plugin.onTargetEndpoint(target, function (e, result) { + plugin.onTargetEndpoint(target, function (e, foundIssues) { assert.equal(e, undefined, e ? " error " : " no error"); - debug(`result: ${result}`); + debug(`foundIssues: ${foundIssues}`); if (messages && messages.length) { debug(`messages: ${util.format(messages)}`); - assert.equal(result, true); + assert.equal(foundIssues, true); assert.equal( target.report.messages.length, messages.length, @@ -50,7 +50,7 @@ const assert = require("assert"), ); }); } else { - assert.equal(result, false, util.format(target.report.messages)); + assert.equal(foundIssues, false, util.format(target.report.messages)); assert.equal( target.report.messages.length, 0, @@ -83,7 +83,7 @@ describe(`${testID} - ${plugin.plugin.name}`, function () { test( 30, - "SSLInfo Enabled = false", + "SSLInfo/Enabled = false, scheme=https", ` @@ -97,7 +97,7 @@ describe(`${testID} - ${plugin.plugin.name}`, function () { test( 40, - "SSLInfo Enabled = true, no truststore", + "SSLInfo/Enabled=true, no truststore (flagged by TD007), profile=apigee", ` @@ -111,7 +111,7 @@ describe(`${testID} - ${plugin.plugin.name}`, function () { test( 41, - "SSLInfo Enabled = true, with TrustStore", + "SSLInfo/Enabled=true, with TrustStore, profile=apigee", ` @@ -124,9 +124,23 @@ describe(`${testID} - ${plugin.plugin.name}`, function () { null ); + test( + 42, + "SSLInfo/Enabled=true, scheme=http, profile=apigee", + ` + + + true + + http://insecure.foo.com/apis/{api_name}/maskconfigs + + `, + ["SSLInfo configuration must not use the Enabled=true with insecure URL"] + ); + testApigeeX( 90, - "SSLInfo with Enforce", + "SSLInfo/Enforce = true, scheme=https, profile=apigeex", ` @@ -142,7 +156,7 @@ describe(`${testID} - ${plugin.plugin.name}`, function () { testApigeeX( 91, - "SSLInfo with Enforce", + "SSLInfo/Enforce=false, scheme=https, profile=apigeex", ` @@ -155,9 +169,24 @@ describe(`${testID} - ${plugin.plugin.name}`, function () { ["SSLInfo configuration does not use Enforce=true"] ); - test( + testApigeeX( 92, - "SSLInfo with Enforce - not ApigeeX", + "SSLInfo/Enforce=true, scheme=http, profile=apigeex", + ` + + + true + truststore1 + + http://insecure.foo.com/apis/{api_name}/maskconfigs + + `, + ["SSLInfo configuration must not use the Enforce=true with insecure URL"] + ); + + test( + 93, + "SSLInfo/Enforce=true, scheme=https, profile=apigee", ` diff --git a/test/specs/TD008-test.js b/test/specs/TD008-test.js index 8528841..bb00e67 100644 --- a/test/specs/TD008-test.js +++ b/test/specs/TD008-test.js @@ -26,7 +26,7 @@ const testID = "TD008", Endpoint = require("../../lib/package/Endpoint.js"), Dom = require("@xmldom/xmldom").DOMParser, rootDir = path.resolve(__dirname, "../fixtures/resources/TD008"), - debug = require("debug")("apigeelint:" + testID); + debug = require("debug")(`apigeelint:${testID}-test`); const loadEndpoint = (sourceDir, shortFileName) => { const fqPath = path.join(sourceDir, shortFileName), diff --git a/test/specs/TD009-test.js b/test/specs/TD009-test.js index 466c2f4..cc84e71 100644 --- a/test/specs/TD009-test.js +++ b/test/specs/TD009-test.js @@ -26,7 +26,7 @@ const testID = "TD009", Endpoint = require("../../lib/package/Endpoint.js"), Dom = require("@xmldom/xmldom").DOMParser, rootDir = path.resolve(__dirname, "../fixtures/resources/TD009"), - debug = require("debug")("apigeelint:" + testID); + debug = require("debug")(`apigeelint:${testID}-test`); const loadEndpoint = (sourceDir, shortFileName) => { const fqPath = path.join(sourceDir, shortFileName), diff --git a/test/specs/TD010-test.js b/test/specs/TD010-test.js index f62e456..5a77d5c 100644 --- a/test/specs/TD010-test.js +++ b/test/specs/TD010-test.js @@ -26,7 +26,7 @@ const testID = "TD010", Endpoint = require("../../lib/package/Endpoint.js"), Dom = require("@xmldom/xmldom").DOMParser, rootDir = path.resolve(__dirname, "../fixtures/resources/TD010"), - debug = require("debug")("apigeelint:" + testID); + debug = require("debug")(`apigeelint:${testID}-test`); const loadEndpoint = (sourceDir, shortFileName) => { const fqPath = path.join(sourceDir, shortFileName), diff --git a/test/specs/TD011-test.js b/test/specs/TD011-test.js index ee186c6..8a096c1 100644 --- a/test/specs/TD011-test.js +++ b/test/specs/TD011-test.js @@ -26,7 +26,7 @@ const testID = "TD011", Endpoint = require("../../lib/package/Endpoint.js"), Dom = require("@xmldom/xmldom").DOMParser, rootDir = path.resolve(__dirname, "../fixtures/resources/TD011"), - debug = require("debug")("apigeelint:" + testID); + debug = require("debug")(`apigeelint:${testID}-test`); const loadEndpoint = (sourceDir, shortFileName) => { const fqPath = path.join(sourceDir, shortFileName), diff --git a/test/specs/TD012-test.js b/test/specs/TD012-test.js index dc9fb5d..04afc44 100644 --- a/test/specs/TD012-test.js +++ b/test/specs/TD012-test.js @@ -26,7 +26,7 @@ const testID = "TD012", Endpoint = require("../../lib/package/Endpoint.js"), Dom = require("@xmldom/xmldom").DOMParser, rootDir = path.resolve(__dirname, "../fixtures/resources/TD012"), - debug = require("debug")("apigeelint:" + testID); + debug = require("debug")(`apigeelint:${testID}-test`); const loadEndpoint = (sourceDir, shortFileName) => { const fqPath = path.join(sourceDir, shortFileName), diff --git a/test/specs/TD013-test.js b/test/specs/TD013-test.js index 68d0e51..675a045 100644 --- a/test/specs/TD013-test.js +++ b/test/specs/TD013-test.js @@ -26,7 +26,7 @@ const testID = "TD013", Endpoint = require("../../lib/package/Endpoint.js"), Dom = require("@xmldom/xmldom").DOMParser, rootDir = path.resolve(__dirname, "../fixtures/resources/TD013"), - debug = require("debug")("apigeelint:" + testID); + debug = require("debug")(`apigeelint:${testID}-test`); const loadEndpoint = (sourceDir, shortFileName) => { const fqPath = path.join(sourceDir, shortFileName), diff --git a/test/specs/TD014-test.js b/test/specs/TD014-test.js index ff0bd59..2b04449 100644 --- a/test/specs/TD014-test.js +++ b/test/specs/TD014-test.js @@ -26,7 +26,7 @@ const testID = "TD014", Endpoint = require("../../lib/package/Endpoint.js"), Dom = require("@xmldom/xmldom").DOMParser, rootDir = path.resolve(__dirname, "../fixtures/resources/TD014"), - debug = require("debug")("apigeelint:" + testID); + debug = require("debug")(`apigeelint:${testID}-test`); const loadEndpoint = (sourceDir, shortFileName) => { const fqPath = path.join(sourceDir, shortFileName), diff --git a/test/specs/TD015-test.js b/test/specs/TD015-test.js new file mode 100644 index 0000000..4257107 --- /dev/null +++ b/test/specs/TD015-test.js @@ -0,0 +1,93 @@ +/* + Copyright 2019-2024 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. +*/ + +/* global describe, it */ + +const testID = "TD015", + assert = require("assert"), + fs = require("fs"), + util = require("util"), + path = require("path"), + bl = require("../../lib/package/bundleLinter.js"), + plugin = require(bl.resolvePlugin(testID)), + Endpoint = require("../../lib/package/Endpoint.js"), + Dom = require("@xmldom/xmldom").DOMParser, + rootDir = path.resolve(__dirname, "../fixtures/resources/TD015"), + debug = require("debug")(`apigeelint:${testID}-test`); + +const loadEndpoint = (sourceDir, shortFileName) => { + const fqPath = path.join(sourceDir, shortFileName), + xml = fs.readFileSync(fqPath).toString("utf-8"), + doc = new Dom().parseFromString(xml), + endpoint = new Endpoint(doc.documentElement, null, ""); + endpoint.getFileName = () => shortFileName; + return endpoint; +}; + +describe(`${testID} - endpoint passes exactly one URL or LoadBalancer check`, function () { + const sourceDir = path.join(rootDir, "pass"); + const testOne = (shortFileName) => { + const endpoint = loadEndpoint(sourceDir, shortFileName); + + it(`check ${shortFileName} passes`, () => { + plugin.onTargetEndpoint(endpoint, (e, foundIssues) => { + assert.equal(e, undefined, "should be undefined"); + const messages = endpoint.getReport().messages; + debug(util.format(messages)); + assert.equal(foundIssues, false, "should be no issues"); + assert.ok(messages, "messages should exist"); + assert.equal(messages.length, 0, "unexpected number of messages"); + }); + }); + }; + + const candidates = fs + .readdirSync(sourceDir) + .filter((shortFileName) => shortFileName.endsWith(".xml")); + + it(`checks that there are tests`, () => { + assert.ok(candidates.length > 0, "tests should exist"); + }); + + candidates.forEach(testOne); +}); + +describe(`${testID} - endpoint does not pass exactly one URL or LoadBalancer check`, () => { + const sourceDir = path.join(rootDir, "fail"); + + const testOne = (shortFileName) => { + const policy = loadEndpoint(sourceDir, shortFileName); + it(`check ${shortFileName} throws error`, () => { + plugin.onTargetEndpoint(policy, (e, foundIssues) => { + assert.equal(undefined, e, "should be undefined"); + assert.equal(true, foundIssues, "should be issues"); + const messages = policy.getReport().messages; + assert.ok(messages, "messages for issues should exist"); + debug(util.format(messages)); + }); + }); + }; + + const candidates = fs + .readdirSync(sourceDir) + .filter((shortFileName) => shortFileName.endsWith(".xml")); + + it(`checks that there are tests`, () => { + assert.ok(candidates.length > 0, "tests should exist"); + }); + + candidates.forEach(testOne); +});