diff --git a/javascript/ql/lib/change-notes/2025-03-24-got-package.md b/javascript/ql/lib/change-notes/2025-03-24-got-package.md new file mode 100644 index 000000000000..4830ce077cbd --- /dev/null +++ b/javascript/ql/lib/change-notes/2025-03-24-got-package.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Improved support for `got` package with `Options`, `paginate()` and `extend()` diff --git a/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll b/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll index d4508da39021..c2b01cf73178 100644 --- a/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll +++ b/javascript/ql/lib/semmle/javascript/frameworks/ClientRequests.qll @@ -414,20 +414,74 @@ module ClientRequest { } } + /** + * Represents an instance of the `got` HTTP client library. + */ + abstract private class GotInstance extends API::Node { + /** + * Gets the options object associated with this instance of `got`. + */ + API::Node getOptions() { none() } + } + + /** + * Represents the root `got` module import. + * For example: `const got = require('got')`. + */ + private class RootGotInstance extends GotInstance { + RootGotInstance() { this = API::moduleImport("got") } + } + + /** + * Represents an instance of `got` created by calling the `extend()` method. + * It may also be chained with multiple calls to `extend()`. + * + * For example: `const client = got.extend({ prefixUrl: 'https://example.com' })`. + */ + private class ExtendGotInstance extends GotInstance { + private GotInstance base; + private API::CallNode extendCall; + + ExtendGotInstance() { + extendCall = base.getMember("extend").getACall() and + this = extendCall.getReturn() + } + + override API::Node getOptions() { + result = extendCall.getParameter(0) or result = base.getOptions() + } + } + /** * A model of a URL request made using the `got` library. */ class GotUrlRequest extends ClientRequest::Range { + GotInstance got; + GotUrlRequest() { - exists(API::Node callee, API::Node got | this = callee.getACall() | - got = [API::moduleImport("got"), API::moduleImport("got").getMember("extend").getReturn()] and - callee = [got, got.getMember(["stream", "get", "post", "put", "patch", "head", "delete"])] + exists(API::Node callee | this = callee.getACall() | + callee = + [ + got, + got.getMember(["stream", "get", "post", "put", "patch", "head", "delete", "paginate"]) + ] ) } override DataFlow::Node getUrl() { result = this.getArgument(0) and not exists(this.getOptionArgument(1, "baseUrl")) + or + // Handle URL from options passed to extend() + result = got.getOptions().getMember("url").asSink() and + not exists(this.getArgument(0)) + or + // Handle URL from options passed as third argument when first arg is undefined/missing + exists(API::InvokeNode optionsCall | + optionsCall = API::moduleImport("got").getMember("Options").getAnInvocation() and + optionsCall.getReturn().getAValueReachableFromSource() = this.getAnArgument() and + result = optionsCall.getParameter(0).getMember("url").asSink() + ) } override DataFlow::Node getHost() { diff --git a/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected b/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected index f9ab265e10d8..bb3a73004536 100644 --- a/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected +++ b/javascript/ql/test/library-tests/frameworks/ClientRequests/ClientRequests.expected @@ -97,6 +97,12 @@ test_ClientRequest | tst.js:319:5:319:26 | superag ... ', url) | | tst.js:320:5:320:23 | superagent.del(url) | | tst.js:321:5:321:32 | superag ... st(url) | +| tst.js:328:5:328:38 | got(und ... ptions) | +| tst.js:329:5:329:49 | got(und ... {url})) | +| tst.js:332:5:332:46 | got.ext ... ).get() | +| tst.js:334:5:334:25 | got.pag ... rl, {}) | +| tst.js:337:5:337:20 | jsonClient.get() | +| tst.js:340:5:340:21 | jsonClient2.get() | test_getADataNode | axiosTest.js:12:5:17:6 | axios({ ... \\n }) | axiosTest.js:15:18:15:55 | { 'Cont ... json' } | | axiosTest.js:12:5:17:6 | axios({ ... \\n }) | axiosTest.js:16:15:16:35 | {x: 'te ... 'test'} | @@ -254,6 +260,14 @@ test_getUrl | tst.js:319:5:319:26 | superag ... ', url) | tst.js:319:23:319:25 | url | | tst.js:320:5:320:23 | superagent.del(url) | tst.js:320:20:320:22 | url | | tst.js:321:5:321:32 | superag ... st(url) | tst.js:321:29:321:31 | url | +| tst.js:328:5:328:38 | got(und ... ptions) | tst.js:327:34:327:36 | url | +| tst.js:328:5:328:38 | got(und ... ptions) | tst.js:328:9:328:17 | undefined | +| tst.js:329:5:329:49 | got(und ... {url})) | tst.js:329:9:329:17 | undefined | +| tst.js:329:5:329:49 | got(und ... {url})) | tst.js:329:44:329:46 | url | +| tst.js:334:5:334:25 | got.pag ... rl, {}) | tst.js:334:18:334:20 | url | +| tst.js:337:5:337:20 | jsonClient.get() | tst.js:336:41:336:43 | url | +| tst.js:340:5:340:21 | jsonClient2.get() | tst.js:339:42:339:44 | url | +| tst.js:340:5:340:21 | jsonClient2.get() | tst.js:339:61:339:63 | url | test_getAResponseDataNode | axiosTest.js:4:5:7:6 | axios({ ... \\n }) | axiosTest.js:4:5:7:6 | axios({ ... \\n }) | json | true | | axiosTest.js:12:5:17:6 | axios({ ... \\n }) | axiosTest.js:12:5:17:6 | axios({ ... \\n }) | json | true | @@ -334,3 +348,9 @@ test_getAResponseDataNode | tst.js:319:5:319:26 | superag ... ', url) | tst.js:319:5:319:26 | superag ... ', url) | stream | true | | tst.js:320:5:320:23 | superagent.del(url) | tst.js:320:5:320:23 | superagent.del(url) | stream | true | | tst.js:321:5:321:32 | superag ... st(url) | tst.js:321:5:321:32 | superag ... st(url) | stream | true | +| tst.js:328:5:328:38 | got(und ... ptions) | tst.js:328:5:328:38 | got(und ... ptions) | text | true | +| tst.js:329:5:329:49 | got(und ... {url})) | tst.js:329:5:329:49 | got(und ... {url})) | text | true | +| tst.js:332:5:332:46 | got.ext ... ).get() | tst.js:332:5:332:46 | got.ext ... ).get() | text | true | +| tst.js:334:5:334:25 | got.pag ... rl, {}) | tst.js:334:5:334:25 | got.pag ... rl, {}) | text | true | +| tst.js:337:5:337:20 | jsonClient.get() | tst.js:337:5:337:20 | jsonClient.get() | text | true | +| tst.js:340:5:340:21 | jsonClient2.get() | tst.js:340:5:340:21 | jsonClient2.get() | text | true | diff --git a/javascript/ql/test/library-tests/frameworks/ClientRequests/tst.js b/javascript/ql/test/library-tests/frameworks/ClientRequests/tst.js index 48c7d7786234..c9fc40dc5068 100644 --- a/javascript/ql/test/library-tests/frameworks/ClientRequests/tst.js +++ b/javascript/ql/test/library-tests/frameworks/ClientRequests/tst.js @@ -320,3 +320,22 @@ function useSuperagent(url){ superagent.del(url); superagent.agent().post(url).send(data); } + +import { Options } from 'got'; + +function gotTests(url){ + const options = new Options({url}); + got(undefined, undefined, options); + got(undefined, undefined, new Options({url})); + + const options2 = new Options({url}); + got.extend(options2).extend(options).get(); + + got.paginate(url, {}); + + const jsonClient = got.extend({url: url}); + jsonClient.get(); + + const jsonClient2 = got.extend({url: url}).extend({url: url}); + jsonClient2.get(); +}