Files
codeql/shared/yaml/codeql/serverless/ServerLess.qll
Rasmus Lerchedahl Petersen 4d2ce6b2e0 python: create shared serverless module and use it
Modelled on the javascript serverless module, but
- The predicate that reports YAML files is now public
  so languages can implement their own file conventions.
- It also reports framework and runtime.
- The conveninece predicates with files still exist,
  but they only report the path.
- Handler mapping conventions are now documented.
- Use parameterised serverless module in Python,
  tests now pass.
2023-07-12 16:42:01 +02:00

161 lines
5.2 KiB
Plaintext

/**
* Provides classes and predicates for working with serverless handlers.
* E.g. [AWS](https://docs.aws.amazon.com/lambda/latest/dg/nodejs-handler.html) or [serverless](https://npmjs.com/package/serverless)
*/
/**
* Provides the input for the `ServerLess` module.
* Most of these should be provided by the `yaml` library.
*/
signature module Input {
// --------------------------------------------------
// The below should be provided by the `yaml` library.
// --------------------------------------------------
class Container {
string getAbsolutePath();
Container getParentContainer();
}
class File extends Container;
class YamlNode {
File getFile();
YamlCollection getParentNode();
}
class YamlValue extends YamlNode;
class YamlCollection extends YamlValue;
class YamlScalar extends YamlValue {
string getValue();
}
class YamlMapping extends YamlCollection {
YamlValue lookup(string key);
YamlValue getValue(int i);
}
}
/**
* Provides classes and predicates for working with serverless handlers.
* Supports AWS, Alibaba, and serverless.
*
* Common usage is to interpret the handlers as functions and add the
* first argument of these as remote flow sources.
*/
module ServerLess<Input I> {
import I
/**
* Gets the looked up value as a convenience.
*/
pragma[inline]
private string lookupValue(YamlMapping mapping, string property) {
result = mapping.lookup(property).(YamlScalar).getValue()
}
/**
* Gets a string where an ending "/." is simplified to "/" (if it exists).
*/
bindingset[base]
private string removeTrailingDot(string base) {
if base.regexpMatch(".*/\\.")
then result = base.substring(0, base.length() - 1)
else result = base
}
/**
* Gets a string where a leading "./" is simplified to "" (if it exists).
*/
bindingset[base]
private string removeLeadingDotSlash(string base) {
if base.regexpMatch("\\./.*") then result = base.substring(2, base.length()) else result = base
}
/**
* Gets a string suitable as part of a file path.
*/
bindingset[base]
private string normalise(string base) { result = removeLeadingDotSlash(removeTrailingDot(base)) }
/**
* Holds if the `.yml` file `ymlFile` contains a serverless configuration fro `framework` with
* `handler`, `codeURI`, and `runtime` properties.
* `codeURI` and `runtime` default to the empty string if no explicit value is set in the configuration.
*
* `handler` should be interpreted in a language specific way, see `mapping.md`.
*/
predicate hasServerlessHandler(
File ymlFile, string framework, string handler, string codeUri, string runtime
) {
exists(YamlMapping resource | ymlFile = resource.getFile() |
// There exists at least "AWS::Serverless::Function" and "Aliyun::Serverless::Function"
resource.lookup("Type").(YamlScalar).getValue().regexpMatch(".*::Serverless::Function") and
framework = lookupValue(resource, "Type") and
exists(YamlMapping properties | properties = resource.lookup("Properties") |
(
handler = lookupValue(properties, "Handler") and
(
if exists(properties.lookup("CodeUri"))
then codeUri = normalise(lookupValue(properties, "CodeUri"))
else codeUri = ""
) and
(
if exists(properties.lookup("Runtime"))
then runtime = lookupValue(properties, "Runtime")
else runtime = ""
)
)
)
or
// The `serverless` library, which specifies a top-level `functions` property
framework = "Serverless" and
exists(YamlMapping functions |
functions = resource.lookup("functions") and
not exists(resource.getParentNode()) and
handler = lookupValue(functions.getValue(_), "handler") and
codeUri = "" and
(
if exists(functions.lookup("Runtime"))
then runtime = lookupValue(functions, "Runtime")
else runtime = ""
)
)
)
}
/**
* Holds if `handler` = `filePart . astPart` and `filePart` does not contain a `.`.
* This is a convenience predicate, as in many cases the first part of the handler property
* should be interpreted as (the stem of) a file name.
*/
bindingset[handler]
predicate splitHandler(string handler, string filePart, string astPart) {
exists(string pattern | pattern = "(.*?)\\.(.*)" |
filePart = handler.regexpCapture(pattern, 1) and
astPart = handler.regexpCapture(pattern, 2)
)
}
/**
* Holds if a file with stem `fileStem` has a serverless handler denoted by `func`.
*
* This is a convenience predicate for the common case where the first part of the
* handler property is the file name.
*
* `func` should be interpreted in a language specific way, see `mapping.md`.
*/
predicate hasServerlessHandler(string fileStem, string func, string framework, string runtime) {
exists(File ymlFile, string handler, string codeUri, string filePart |
hasServerlessHandler(ymlFile, framework, handler, codeUri, runtime)
|
splitHandler(handler, filePart, func) and
fileStem = ymlFile.getParentContainer().getAbsolutePath() + "/" + codeUri + filePart
)
}
}