diff --git a/.gitignore b/.gitignore index e705ba6f7fd1..d9a01a3322ce 100644 --- a/.gitignore +++ b/.gitignore @@ -102,3 +102,4 @@ flake.lock ############## user.bazelrc bazel-* +/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua-report/ diff --git a/build.sbt b/build.sbt index 715eeb74c1ab..7b79adc0692c 100644 --- a/build.sbt +++ b/build.sbt @@ -25,6 +25,7 @@ lazy val swiftsrc2cpg = Projects.swiftsrc2cpg lazy val csharpsrc2cpg = Projects.csharpsrc2cpg lazy val abap2cpg = Projects.abap2cpg lazy val rust2cpg = Projects.rust2cpg +lazy val lua2cpg = Projects.lua2cpg lazy val linterRules = Projects.linterRules lazy val root = project @@ -51,6 +52,7 @@ lazy val root = project csharpsrc2cpg, abap2cpg, rust2cpg, + lua2cpg, linterRules ) .dependsOn(linterRules % ScalafixConfig) diff --git a/joern-cli/build.sbt b/joern-cli/build.sbt index a897ebf5655c..2624bed7b762 100644 --- a/joern-cli/build.sbt +++ b/joern-cli/build.sbt @@ -48,6 +48,7 @@ lazy val rubysrc2cpg = project.in(file("frontends/rubysrc2cpg")) lazy val gosrc2cpg = project.in(file("frontends/gosrc2cpg")) lazy val csharpsrc2cpg = project.in(file("frontends/csharpsrc2cpg")) lazy val rust2cpg = project.in(file("frontends/rust2cpg")) +lazy val lua2cpg = project.in(file("frontends/lua2cpg")) Universal / mappings ++= frontendMappings("kotlin2cpg", (kotlin2cpg / stage).value) Universal / mappings ++= frontendMappings("abap2cpg", (abap2cpg / stage).value) @@ -63,6 +64,7 @@ Universal / mappings ++= frontendMappings("rubysrc2cpg", (rubysrc2cpg / stage).v Universal / mappings ++= frontendMappings("gosrc2cpg", (gosrc2cpg / stage).value) Universal / mappings ++= frontendMappings("csharpsrc2cpg", (csharpsrc2cpg / stage).value) Universal / mappings ++= frontendMappings("rust2cpg", (rust2cpg / stage).value) +Universal / mappings ++= frontendMappings("lua2cpg", (lua2cpg / stage).value) lazy val cpgVersionFile = taskKey[File]("persist cpg version in file (e.g. for schema-extender)") cpgVersionFile := { diff --git a/joern-cli/frontends/lua2cpg/README.md b/joern-cli/frontends/lua2cpg/README.md new file mode 100644 index 000000000000..8568a9f975fb --- /dev/null +++ b/joern-cli/frontends/lua2cpg/README.md @@ -0,0 +1,218 @@ +# lua2cpg + +`lua2cpg` is a Lua 5.1 analyzer for Joern. It builds Code Property +Graphs for Lua programs through a bytecode-based pipeline and emits +taint-analysis evidence for Lua code, including interprocedural flow, +source/sink matches, sanitizer classifications, and vulnerability reports. + +The analysis core operates on Lua 5.1 bytecode. To analyze Lua source files, +compile them with `luac5.1` and pass the generated `.luac` files to +`lua2cpg`. Source files placed in the input tree are also recorded in the CPG +file inventory. + +## Prerequisites + +- Use JDK 21 and `sbt` 1.12.5, matching the Joern repository configuration. +- Run the commands below from the Joern repository root. +- Install `luac5.1` when starting from Lua source files. + +## Build + +Build the staged frontend command: + +```bash +sbt 'lua2cpg/stage' +``` + +The staged command is written to: + +```bash +joern-cli/frontends/lua2cpg/target/universal/stage/bin/lua2cpg +``` + +Check the available options: + +```bash +joern-cli/frontends/lua2cpg/target/universal/stage/bin/lua2cpg --help +``` + +## Quickstart + +Analyze an existing Lua 5.1 bytecode directory: + +```bash +LUA2CPG=joern-cli/frontends/lua2cpg/target/universal/stage/bin/lua2cpg +INPUT=/path/to/lua-bytecode +OUTPUT=/tmp/lua.cpg.bin + +"$LUA2CPG" "$INPUT" --output "$OUTPUT" +``` + +Analyze a Lua source file by compiling it to Lua 5.1 bytecode first: + +```bash +mkdir -p /tmp/lua-bytecode +luac5.1 -o /tmp/lua-bytecode/app.luac /path/to/app.lua + +joern-cli/frontends/lua2cpg/target/universal/stage/bin/lua2cpg \ + /tmp/lua-bytecode \ + --output /tmp/lua.cpg.bin +``` + +For a source tree, preserve the directory layout while compiling: + +```bash +SRC_ROOT=/path/to/lua-source +BC_ROOT=/tmp/lua-bytecode + +mkdir -p "$BC_ROOT" +find "$SRC_ROOT" -name '*.lua' -print0 | + while IFS= read -r -d '' file; do + rel="${file#$SRC_ROOT/}" + out="$BC_ROOT/${rel%.lua}.luac" + mkdir -p "$(dirname "$out")" + luac5.1 -o "$out" "$file" + done + +joern-cli/frontends/lua2cpg/target/universal/stage/bin/lua2cpg \ + "$BC_ROOT" \ + --output /tmp/lua.cpg.bin +``` + +## Inspecting The CPG + +Open the generated CPG with Joern and inspect the Lua-specific evidence: + +```bash +joern /tmp/lua.cpg.bin +``` + +Useful traversals: + +```scala +cpg.metaData.language.l +cpg.file.name.l +cpg.method.fullName.l +cpg.call.nameExact("lua.module.resolution").code.l +cpg.call.nameExact("lua.calltarget.candidate").code.l +cpg.call.nameExact("lua.source.endpoint").code.l +cpg.call.nameExact("lua.sink.endpoint").code.l +cpg.call.nameExact("lua.sanitizer.classification").code.l +cpg.call.nameExact("lua.report.vulnerability").code.l +``` + +## Taint Evidence Export + +`lua2cpg` can also write reviewer-visible JSON evidence for benchmark and +debugging workflows: + +```bash +joern-cli/frontends/lua2cpg/target/universal/stage/bin/lua2cpg \ + /path/to/lua-bytecode \ + --output /tmp/lua.cpg.bin \ + --lua-real-firmware-output-dir /tmp/lua2cpg-evidence +``` + +The evidence directory contains: + +- `decoder-summary.json`: input, decode, prototype, instruction, callsite, and + local-flow totals. +- `path-search-profile.json`: taint path-search counters and retained pair + profiles. +- `run-summary.json`: native run status and decode totals. +- `run-errors.json`: native run errors. +- `staging/*.json`: per-artifact decode, flow, module-resolution, source/sink, + sanitizer, and path-evidence rows. + +This export is optional. The primary `lua2cpg` output is the CPG written by +`--output`. + +## Self-Contained Reviewer Corpus + +The test resources include a self-contained OpenWrt-derived Lua corpus for +review and regression checks: + +```text +joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua +``` + +The corpus preserves the original `usr/lib/lua` layout and contains: + +- 42 `.lua` source files recorded in the CPG file inventory. +- 42 Lua 5.1 `.luac` bytecode files analyzed by the bytecode pipeline. + +To generate a native JSON analysis report from the Joern repository root: + +```bash +joern-cli/frontends/lua2cpg/target/universal/stage/bin/lua2cpg \ + joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua \ + --output /tmp/openwrt-derived-firmware-lua.cpg.bin \ + --lua-real-firmware-output-dir /tmp/openwrt-derived-firmware-lua-report +``` + +The generated report and CPG are reproducible outputs and are intentionally not +tracked by Git. A successful corpus run decodes all 42 bytecode inputs with no +diagnostics and emits 164 source endpoints, 94 sink endpoints, 20 taint paths, +and 20 reports. + +## Supported Analysis + +- Lua version: Lua 5.1. +- Inputs: Lua 5.1 `.luac` bytecode; Lua source after compilation with + `luac5.1`. +- CPG content: file inventory, bytecode artifacts, prototypes, constants, + instructions, methods, identifiers, calls, and reaching definitions. +- Program analysis: intraprocedural value flow, module require/return-table + linkage, cross-module call targets, interprocedural argument and return flow, + source/sink matching, sanitizer classification, and vulnerability reports. +- Distribution boundary: this frontend does not ship QueryDB queries as part + of this README. Reviewer-visible results are exposed through CPG nodes and + optional JSON evidence. + +## Architecture + +The frontend is organized around a bytecode-first analysis pipeline: + +- `LuaFileInventoryPass` records `.lua` source files in the CPG file inventory. +- `LuaBytecodeDecoder` decodes Lua 5.1 bytecode artifacts into profiles, + prototypes, constants, instructions, and diagnostics. +- `LuaBytecodeModelPass` emits the decoded bytecode model into the CPG. +- `LuaInstructionSemantics` computes prototype-local value, call, table, + global, upvalue, and boundary facts. +- `LuaProgramSemantics` normalizes module-level and interprocedural flow, + source/sink, sanitizer, and report evidence across decoded artifacts. +- `LuaRealFirmwareEvidenceExporter` writes the optional JSON evidence directory + requested by `--lua-real-firmware-output-dir`. + +## Tests + +Use the smallest tier that answers the review question first. + +Quick smoke: + +```bash +sbt 'lua2cpg/testOnly io.joern.lua2cpg.RulesSanitizerReportSmokeTest' +sbt 'lua2cpg/stage' +joern-cli/frontends/lua2cpg/target/universal/stage/bin/lua2cpg --help +git status --short +``` + +Focused capability tests: + +```bash +sbt 'lua2cpg/testOnly io.joern.lua2cpg.BytecodeModelSmokeTest' +sbt 'lua2cpg/testOnly io.joern.lua2cpg.IntraproceduralSemanticsSmokeTest' +sbt 'lua2cpg/testOnly io.joern.lua2cpg.InterproceduralModuleTaintSmokeTest' +sbt 'lua2cpg/testOnly io.joern.lua2cpg.RulesSanitizerReportSmokeTest' +sbt 'lua2cpg/testOnly io.joern.lua2cpg.OpenWrtDerivedFirmwareCorpusSmokeTest' +sbt 'lua2cpg/testOnly io.joern.lua2cpg.RealFirmwareEvidenceExportSmokeTest' +``` + +Full frontend test suite: + +```bash +sbt 'lua2cpg/test' +``` + +The full suite covers the decoder, CPG modeling, program semantics, evidence +export, and self-contained corpus regression tests. diff --git a/joern-cli/frontends/lua2cpg/build.sbt b/joern-cli/frontends/lua2cpg/build.sbt new file mode 100644 index 000000000000..714cba0cb07c --- /dev/null +++ b/joern-cli/frontends/lua2cpg/build.sbt @@ -0,0 +1,13 @@ +name := "lua2cpg" + +dependsOn( + Projects.x2cpg % "compile->compile;test->test", + Projects.linterRules % ScalafixConfig +) + +libraryDependencies ++= Seq( + "com.lihaoyi" %% "ujson" % Versions.upickle, + "org.scalatest" %% "scalatest" % Versions.scalatest % Test +) + +enablePlugins(JavaAppPackaging, LauncherJarPlugin) diff --git a/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/Lua2Cpg.scala b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/Lua2Cpg.scala new file mode 100644 index 000000000000..6a491c504e0d --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/Lua2Cpg.scala @@ -0,0 +1,26 @@ +package io.joern.lua2cpg + +import io.joern.lua2cpg.bytecode.{LuaProgramSemantics, LuaRealFirmwareEvidenceExporter} +import io.joern.lua2cpg.passes.{LuaBytecodeModelPass, LuaFileInventoryPass} +import io.joern.x2cpg.X2Cpg.withNewEmptyCpg +import io.joern.x2cpg.X2CpgFrontend +import io.joern.x2cpg.passes.frontend.MetaDataPass +import io.shiftleft.codepropertygraph.generated.Cpg + +import scala.util.Try + +class Lua2Cpg extends X2CpgFrontend { + override type ConfigType = Config + override val defaultConfig: Config = Config() + + override def createCpg(config: Config): Try[Cpg] = { + val decoded = LuaBytecodeModelPass.decodeInputs(config) + val programSemantics = LuaProgramSemantics.normalize(decoded.map(item => item.relativeName -> item.result)) + withNewEmptyCpg(config.outputPath, config) { (cpg, config) => + new MetaDataPass(cpg, "LUA", config.inputPath).createAndApply() + new LuaFileInventoryPass(cpg, config).createAndApply() + new LuaBytecodeModelPass(cpg, config, Some(decoded)).createAndApply() + LuaRealFirmwareEvidenceExporter.write(config, decoded, programSemantics) + } + } +} diff --git a/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/Main.scala b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/Main.scala new file mode 100644 index 000000000000..4a8ac6fa59c6 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/Main.scala @@ -0,0 +1,31 @@ +package io.joern.lua2cpg + +import io.joern.lua2cpg.Frontend.cmdLineParser +import io.joern.x2cpg.{X2CpgConfig, X2CpgMain} +import scopt.OParser + +final case class Config( + realFirmwareOutputDir: Option[String] = None, + override val genericConfig: X2CpgConfig.GenericConfig = X2CpgConfig.GenericConfig() +) extends X2CpgConfig[Config] { + override def withGenericConfig(value: X2CpgConfig.GenericConfig): Config = + copy(genericConfig = value) + + def withRealFirmwareOutputDir(value: String): Config = + copy(realFirmwareOutputDir = Some(value)) +} + +private object Frontend { + val cmdLineParser: OParser[Unit, Config] = { + val builder = OParser.builder[Config] + import builder.* + OParser.sequence( + programName("lua2cpg"), + opt[String]("lua-real-firmware-output-dir") + .text("write Lua real-firmware benchmark evidence JSON to the given directory") + .action((value, config) => config.withRealFirmwareOutputDir(value)) + ) + } +} + +object Main extends X2CpgMain(new Lua2Cpg(), cmdLineParser) diff --git a/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/bytecode/LuaBytecodeDecoder.scala b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/bytecode/LuaBytecodeDecoder.scala new file mode 100644 index 000000000000..9aeea40d4817 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/bytecode/LuaBytecodeDecoder.scala @@ -0,0 +1,316 @@ +package io.joern.lua2cpg.bytecode + +import java.nio.ByteBuffer +import java.nio.ByteOrder +import java.nio.charset.StandardCharsets + +object LuaBytecodeDecoder { + private val LuaMagic: Array[Byte] = Array(0x1b.toByte, 0x4c.toByte, 0x75.toByte, 0x61.toByte) + private val Lua51Version: Int = 0x51 + private val DefaultInputKind: String = "lua-bytecode" + private val SuccessDiagnosticKind = "accepted" + private val SeverityInfo = "info" + private val SeverityError = "error" + private val NumberModeFloating = "floating" + private val NumberModeIntegral = "integral" + private val LuaByteStringCharset = StandardCharsets.ISO_8859_1 + + def decode(path: String, bytes: Array[Byte]): LuaBytecodeDecodeResult = { + val reader = new Reader(bytes) + reader.decode(path) + } + + private final class Reader(bytes: Array[Byte]) { + private var index: Int = 0 + private var version: Int = 0 + private var format: Int = 0 + private var endianFlag: Int = 1 + private var byteOrder: ByteOrder = ByteOrder.LITTLE_ENDIAN + private var intSize: Int = 4 + private var sizeTSize: Int = 8 + private var instructionSize: Int = 4 + private var luaNumberSize: Int = 8 + private var integralFlag: Int = 0 + private var currentProfileId: Option[String] = None + + def decode(path: String): LuaBytecodeDecodeResult = { + if (bytes.length < LuaMagic.length || !bytes.take(LuaMagic.length).sameElements(LuaMagic)) { + return rejected(path, "not-lua-bytecode", "input does not start with Lua bytecode magic") + } + + index = LuaMagic.length + try { + decodeHeader() + val root = decodePrototype("root", None, Vector.empty) + accepted(path, root) + } catch { + case error: DecodeFailure => rejected(path, error.kind, error.message) + } + } + + private def decodeHeader(): Unit = { + version = readByte() + format = readByte() + endianFlag = readByte() + intSize = readByte() + sizeTSize = readByte() + instructionSize = readByte() + luaNumberSize = readByte() + integralFlag = readByte() + + if (version != Lua51Version) { + reject("unsupported-bytecode-version", f"expected Lua bytecode version 0x51, got 0x$version%02x") + } + validateImplementedHeader() + byteOrder = endianFlag match { + case 0 => ByteOrder.BIG_ENDIAN + case 1 => ByteOrder.LITTLE_ENDIAN + } + currentProfileId = Some(profileId) + } + + private def validateImplementedHeader(): Unit = { + if (format != 0) { + reject("unsupported-bytecode-profile", s"unsupported Lua 5.1 bytecode format $format") + } + if (!Set(0, 1).contains(endianFlag)) { + reject("unsupported-bytecode-profile", s"unsupported Lua 5.1 endianness flag $endianFlag") + } + if (intSize <= 0) { + reject("unsupported-bytecode-profile", s"unsupported Lua 5.1 int size $intSize") + } + if (sizeTSize <= 0) { + reject("unsupported-bytecode-profile", s"unsupported Lua 5.1 size_t size $sizeTSize") + } + if (instructionSize != 4) { + reject("unsupported-bytecode-profile", s"unsupported Lua 5.1 instruction size $instructionSize") + } + if (luaNumberSize != 8) { + reject("unsupported-bytecode-profile", s"unsupported Lua 5.1 lua_Number size $luaNumberSize") + } + if (integralFlag != 0) { + reject("unsupported-bytecode-profile", s"unsupported Lua 5.1 number mode integral_flag=$integralFlag") + } + } + + private def decodePrototype( + prototypeId: String, + parentPrototypeId: Option[String], + ordinalPath: Vector[Int] + ): LuaPrototype = { + val sourceName = readString() + val firstLine = readUInt() + val lastLine = readUInt() + val upvalueCount = readByte() + val numParams = readByte() + val isVararg = (readByte() & 0x02) != 0 + val maxStack = readByte() + val instructions = readVector(readUInt(), pc => decodeInstruction(pc)) + val constants = readVector(readUInt(), constantIndex => decodeConstant(constantIndex)) + val nested = readVector( + readUInt(), + childOrdinal => { + val childId = s"$prototypeId.$childOrdinal" + decodePrototype(childId, Some(prototypeId), ordinalPath :+ childOrdinal) + } + ) + val lineNumbers = readVector(readUInt(), _ => readUInt()) + val locals = readVector(readUInt(), _ => LuaLocal(readString(), readUInt(), readUInt())) + val upvalueNames = readVector(readUInt(), _ => readString()) + + LuaPrototype( + prototypeId = prototypeId, + parentPrototypeId = parentPrototypeId, + ordinalPath = ordinalPath, + sourceName = sourceName, + firstLine = firstLine, + lastLine = lastLine, + upvalueCount = upvalueCount, + numParams = numParams, + isVararg = isVararg, + maxStack = maxStack, + instructions = instructions, + constants = constants, + nested = nested, + lineNumbers = lineNumbers, + locals = locals, + upvalueNames = upvalueNames + ) + } + + private def decodeInstruction(pc: Int): LuaInstruction = { + val raw = readUInt32() + val opcodeCode = bits(raw, 0, 6).toInt + val opcode = LuaOpcode.fromCode(opcodeCode).getOrElse { + reject("malformed-constant", s"invalid opcode $opcodeCode at pc $pc") + } + val a = bits(raw, 6, 8).toInt + opcode.mode match { + case LuaInstructionMode.Abc => + LuaInstruction( + pc = pc, + opcode = opcode, + mode = opcode.mode, + a = a, + b = bits(raw, 23, 9).toInt, + c = Some(bits(raw, 14, 9).toInt) + ) + case LuaInstructionMode.ABx => + LuaInstruction(pc = pc, opcode = opcode, mode = opcode.mode, a = a, b = bits(raw, 14, 18).toInt, c = None) + case LuaInstructionMode.AsBx => + LuaInstruction( + pc = pc, + opcode = opcode, + mode = opcode.mode, + a = a, + b = bits(raw, 14, 18).toInt - 131071, + c = None + ) + } + } + + private def decodeConstant(constantIndex: Int): LuaConstant = { + readByte() match { + case 0 => LuaConstant(constantIndex, "nil", LuaConstantValue.NilValue) + case 1 => LuaConstant(constantIndex, "boolean", LuaConstantValue.BooleanValue(readByte() != 0)) + case 3 => LuaConstant(constantIndex, "number", LuaConstantValue.NumberValue(readDouble())) + case 4 => LuaConstant(constantIndex, "string", LuaConstantValue.StringValue(readString())) + case other => + reject("malformed-constant", s"unsupported constant tag $other at index $constantIndex") + } + } + + private def readVector[A](count: Long, decodeElement: Int => A): Vector[A] = { + if (count > Int.MaxValue) { + reject("unsupported-bytecode-profile", s"unsupported Lua 5.1 element count $count") + } + Vector.tabulate(count.toInt)(decodeElement) + } + + private def readByte(): Int = { + ensureAvailable(1) + val value = bytes(index) & 0xff + index += 1 + value + } + + private def readUInt32(): Long = { + ensureAvailable(4) + val value = ByteBuffer.wrap(bytes, index, 4).order(byteOrder).getInt.toLong & 0xffffffffL + index += 4 + value + } + + private def readUInt(): Long = readUnsignedInteger(intSize) + + private def readSizeT(): Long = readUnsignedInteger(sizeTSize) + + private def readUnsignedInteger(size: Int): Long = { + ensureAvailable(size) + val raw = bytes.slice(index, index + size) + index += size + val orderedBytes = if (byteOrder == ByteOrder.LITTLE_ENDIAN) raw.reverse else raw + val value = BigInt(1, orderedBytes) + if (value > BigInt(Long.MaxValue)) { + reject("unsupported-bytecode-profile", s"unsupported Lua 5.1 unsigned integer value $value") + } + value.longValue + } + + private def readDouble(): Double = { + ensureAvailable(luaNumberSize) + val value = ByteBuffer.wrap(bytes, index, luaNumberSize).order(byteOrder).getDouble + index += luaNumberSize + value + } + + private def readString(): LuaByteStringText = { + val size = readSizeT() + if (size == 0) { + return "" + } + if (size > Int.MaxValue) { + reject("malformed-constant", s"unsupported Lua bytecode string size $size") + } + ensureAvailable(size.toInt) + val bytesStart = index + index += size.toInt + if (bytes(bytesStart + size.toInt - 1) != 0) { + reject("malformed-constant", "unterminated Lua bytecode string") + } + new String(bytes, bytesStart, size.toInt - 1, LuaByteStringCharset) + } + + private def ensureAvailable(size: Int): Unit = { + if (size < 0 || index > bytes.length - size) { + reject("truncated-bytecode", "unexpected end of bytecode stream") + } + } + + private def accepted(path: String, root: LuaPrototype): LuaBytecodeDecodeResult = { + val profile = buildProfile() + LuaBytecodeDecodeResult( + artifact = LuaBytecodeArtifact( + path = path, + inputKind = DefaultInputKind, + profileId = Some(profile.profileId), + accepted = true, + diagnostic = LuaDiagnostic( + kind = SuccessDiagnosticKind, + message = "Lua 5.1 bytecode accepted", + severity = SeverityInfo, + successFactsAllowed = true + ) + ), + profile = Some(profile), + root = Some(root) + ) + } + + private def rejected(path: String, kind: String, message: String): LuaBytecodeDecodeResult = { + LuaBytecodeDecodeResult( + artifact = LuaBytecodeArtifact( + path = path, + inputKind = DefaultInputKind, + profileId = currentProfileId, + accepted = false, + diagnostic = + LuaDiagnostic(kind = kind, message = message, severity = SeverityError, successFactsAllowed = false) + ), + profile = currentProfileId.map(_ => buildProfile()), + root = None + ) + } + + private def buildProfile(): LuaBytecodeProfile = { + LuaBytecodeProfile( + luaVersion = "5.1", + bytecodeVersion = f"0x$version%02x", + format = format, + endianness = if (byteOrder == ByteOrder.BIG_ENDIAN) "big-endian" else "little-endian", + intSize = intSize, + sizeTSize = sizeTSize, + instructionSize = instructionSize, + luaNumberSize = luaNumberSize, + numberMode = if (integralFlag == 0) NumberModeFloating else NumberModeIntegral, + profileId = profileId + ) + } + + private def profileId: String = { + val endian = if (byteOrder == ByteOrder.BIG_ENDIAN) "big" else "little" + val numberMode = if (integralFlag == 0) "float" else "integral" + s"lua51-$endian-int$intSize-size_t$sizeTSize-instruction$instructionSize-number$luaNumberSize-$numberMode" + } + + private def bits(number: Long, position: Int, size: Int): Long = { + (number >> position) & ((1L << size) - 1L) + } + + private def reject(kind: String, message: String): Nothing = { + throw DecodeFailure(kind, message) + } + } + + private final case class DecodeFailure(kind: String, message: String) extends RuntimeException(message) +} diff --git a/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/bytecode/LuaBytecodeModel.scala b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/bytecode/LuaBytecodeModel.scala new file mode 100644 index 000000000000..ba2efc791a08 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/bytecode/LuaBytecodeModel.scala @@ -0,0 +1,122 @@ +package io.joern.lua2cpg.bytecode + +/** Lua 5.1 byte strings decoded with ISO-8859-1, preserving each byte as the same numeric code point. */ +type LuaByteStringText = String + +final case class LuaBytecodeArtifact( + path: String, + inputKind: String, + profileId: Option[String], + accepted: Boolean, + diagnostic: LuaDiagnostic +) + +final case class LuaBytecodeProfile( + luaVersion: String, + bytecodeVersion: String, + format: Int, + endianness: String, + intSize: Int, + sizeTSize: Int, + instructionSize: Int, + luaNumberSize: Int, + numberMode: String, + profileId: String +) + +final case class LuaPrototype( + prototypeId: String, + parentPrototypeId: Option[String], + ordinalPath: Vector[Int], + sourceName: LuaByteStringText, + firstLine: Long, + lastLine: Long, + upvalueCount: Int, + numParams: Int, + isVararg: Boolean, + maxStack: Int, + instructions: Vector[LuaInstruction], + constants: Vector[LuaConstant], + nested: Vector[LuaPrototype], + lineNumbers: Vector[Long], + locals: Vector[LuaLocal], + upvalueNames: Vector[LuaByteStringText] +) + +final case class LuaInstruction(pc: Int, opcode: LuaOpcode, mode: LuaInstructionMode, a: Int, b: Int, c: Option[Int]) + +final case class LuaConstant(index: Int, luaType: String, value: LuaConstantValue) + +final case class LuaDiagnostic(kind: String, message: String, severity: String, successFactsAllowed: Boolean) + +final case class LuaLocal(name: LuaByteStringText, startPc: Long, endPc: Long) + +final case class LuaBytecodeDecodeResult( + artifact: LuaBytecodeArtifact, + profile: Option[LuaBytecodeProfile], + root: Option[LuaPrototype] +) + +enum LuaInstructionMode(val encodedName: String) { + case Abc extends LuaInstructionMode("ABC") + case ABx extends LuaInstructionMode("ABx") + case AsBx extends LuaInstructionMode("AsBx") + + override def toString: String = encodedName +} + +enum LuaOpcode(val code: Int, val mode: LuaInstructionMode, val mnemonic: String) { + case Move extends LuaOpcode(0, LuaInstructionMode.Abc, "MOVE") + case LoadK extends LuaOpcode(1, LuaInstructionMode.ABx, "LOADK") + case LoadBool extends LuaOpcode(2, LuaInstructionMode.Abc, "LOADBOOL") + case LoadNil extends LuaOpcode(3, LuaInstructionMode.Abc, "LOADNIL") + case GetUpval extends LuaOpcode(4, LuaInstructionMode.Abc, "GETUPVAL") + case GetGlobal extends LuaOpcode(5, LuaInstructionMode.ABx, "GETGLOBAL") + case GetTable extends LuaOpcode(6, LuaInstructionMode.Abc, "GETTABLE") + case SetGlobal extends LuaOpcode(7, LuaInstructionMode.ABx, "SETGLOBAL") + case SetUpval extends LuaOpcode(8, LuaInstructionMode.Abc, "SETUPVAL") + case SetTable extends LuaOpcode(9, LuaInstructionMode.Abc, "SETTABLE") + case NewTable extends LuaOpcode(10, LuaInstructionMode.Abc, "NEWTABLE") + case Self extends LuaOpcode(11, LuaInstructionMode.Abc, "SELF") + case Add extends LuaOpcode(12, LuaInstructionMode.Abc, "ADD") + case Sub extends LuaOpcode(13, LuaInstructionMode.Abc, "SUB") + case Mul extends LuaOpcode(14, LuaInstructionMode.Abc, "MUL") + case Div extends LuaOpcode(15, LuaInstructionMode.Abc, "DIV") + case Mod extends LuaOpcode(16, LuaInstructionMode.Abc, "MOD") + case Pow extends LuaOpcode(17, LuaInstructionMode.Abc, "POW") + case Unm extends LuaOpcode(18, LuaInstructionMode.Abc, "UNM") + case Not extends LuaOpcode(19, LuaInstructionMode.Abc, "NOT") + case Len extends LuaOpcode(20, LuaInstructionMode.Abc, "LEN") + case Concat extends LuaOpcode(21, LuaInstructionMode.Abc, "CONCAT") + case Jmp extends LuaOpcode(22, LuaInstructionMode.AsBx, "JMP") + case Eq extends LuaOpcode(23, LuaInstructionMode.Abc, "EQ") + case Lt extends LuaOpcode(24, LuaInstructionMode.Abc, "LT") + case Le extends LuaOpcode(25, LuaInstructionMode.Abc, "LE") + case Test extends LuaOpcode(26, LuaInstructionMode.Abc, "TEST") + case TestSet extends LuaOpcode(27, LuaInstructionMode.Abc, "TESTSET") + case Call extends LuaOpcode(28, LuaInstructionMode.Abc, "CALL") + case TailCall extends LuaOpcode(29, LuaInstructionMode.Abc, "TAILCALL") + case Return extends LuaOpcode(30, LuaInstructionMode.Abc, "RETURN") + case ForLoop extends LuaOpcode(31, LuaInstructionMode.AsBx, "FORLOOP") + case ForPrep extends LuaOpcode(32, LuaInstructionMode.AsBx, "FORPREP") + case TForLoop extends LuaOpcode(33, LuaInstructionMode.Abc, "TFORLOOP") + case SetList extends LuaOpcode(34, LuaInstructionMode.Abc, "SETLIST") + case Close extends LuaOpcode(35, LuaInstructionMode.Abc, "CLOSE") + case Closure extends LuaOpcode(36, LuaInstructionMode.ABx, "CLOSURE") + case Vararg extends LuaOpcode(37, LuaInstructionMode.Abc, "VARARG") + + override def toString: String = mnemonic +} + +object LuaOpcode { + private val byCode: Map[Int, LuaOpcode] = LuaOpcode.values.map(opcode => opcode.code -> opcode).toMap + + def fromCode(code: Int): Option[LuaOpcode] = byCode.get(code) +} + +enum LuaConstantValue { + case NilValue + case BooleanValue(value: Boolean) + case NumberValue(value: Double) + case StringValue(value: LuaByteStringText) +} diff --git a/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/bytecode/LuaInstructionSemantics.scala b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/bytecode/LuaInstructionSemantics.scala new file mode 100644 index 000000000000..523d9a62e5c7 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/bytecode/LuaInstructionSemantics.scala @@ -0,0 +1,743 @@ +package io.joern.lua2cpg.bytecode + +final case class LuaRegisterEvent(kind: String, prototypeId: String, pc: Int, slot: Int, valueRef: String) + +final case class LuaSemanticStep(sourceRef: String, destRef: String, kind: String) + +final case class LuaClosureValue(slot: Int, valueRef: String, targetPrototypeId: String, provenance: String) + +final case class LuaCallSite( + callsiteId: String, + prototypeId: String, + pc: Int, + opcode: String, + targetValueRef: String, + firstArgSlot: Option[Int], + argCount: Option[Int], + firstReturnSlot: Option[Int], + returnCount: Option[Int] +) + +final case class LuaLocalFlow(sourceRef: String, sinkRef: String, edgeKind: String, provenance: String) + +final case class LuaTableFieldFlow( + tableRef: String, + keyRef: String, + writeRef: String, + readRef: String, + provenance: String +) + +final case class LuaGlobalFlow( + globalName: String, + writeRef: String, + readRef: String, + valueRef: String, + provenance: String +) + +final case class LuaUpvalueFlow( + upvalueId: String, + captureRef: String, + readRef: String, + writeRef: String, + provenance: String +) + +final case class LuaCallTargetCandidate(callsiteId: String, targetRef: String, confidence: String, provenance: String) + +final case class LuaUnresolvedCall(callsiteId: String, unresolvedReason: String, provenance: String) + +final case class LuaKillOverwrite( + killId: String, + prototypeId: String, + firstWrite: String, + laterWrite: String, + readRef: String, + sinkRef: String, + reason: String +) + +final case class LuaNegativeExpectation( + negativeId: String, + sourceRef: String, + sinkRef: String, + kind: String, + reason: String +) + +final case class LuaPrototypeSemantics( + registerEvents: Vector[LuaRegisterEvent], + semanticSteps: Vector[LuaSemanticStep], + closureValues: Vector[LuaClosureValue], + callSites: Vector[LuaCallSite], + localFlows: Vector[LuaLocalFlow], + tableFieldFlows: Vector[LuaTableFieldFlow], + globalFlows: Vector[LuaGlobalFlow], + upvalueFlows: Vector[LuaUpvalueFlow], + callTargetCandidates: Vector[LuaCallTargetCandidate], + unresolvedCalls: Vector[LuaUnresolvedCall], + killOverwrites: Vector[LuaKillOverwrite], + negativeExpectations: Vector[LuaNegativeExpectation] +) + +object LuaInstructionSemantics { + private val RkConstantBase = 256 + private val BytecodeProvenance = "bytecode-only" + private val BoundaryProvenance = "bytecode-boundary" + private val ParamDerivedReason = "param-derived" + private val MutationBoundary = "upvalue-mutation-boundary" + private val UpvalueStaleBoundary = "no-stale-upvalue-reuse-after-setupval" + + private final case class UpvalueClosureBindings( + directTargets: Map[Int, String], + tableTargets: Map[Int, Map[String, String]] + ) { + def nonEmpty: Boolean = directTargets.nonEmpty || tableTargets.nonEmpty + } + + private object UpvalueClosureBindings { + val Empty: UpvalueClosureBindings = UpvalueClosureBindings(Map.empty, Map.empty) + } + + def normalize(prototype: LuaPrototype): LuaPrototypeSemantics = { + val upvalueClosureBindings = closureBindingsByPrototypeAndUpvalue(prototype) + val builder = Vector.newBuilder[LuaPrototypeSemantics] + def visit(current: LuaPrototype): Unit = { + builder += normalizeOne( + current, + upvalueClosureBindings.getOrElse(current.prototypeId, UpvalueClosureBindings.Empty) + ) + current.nested.foreach(visit) + } + visit(prototype) + combine(builder.result()) + } + + def normalizePrototype(prototype: LuaPrototype): LuaPrototypeSemantics = + normalizeOne(prototype, UpvalueClosureBindings.Empty) + + private def normalizeOne( + prototype: LuaPrototype, + upvalueClosureBindings: UpvalueClosureBindings + ): LuaPrototypeSemantics = { + val state = new SemanticState(prototype, upvalueClosureBindings) + prototype.instructions.sortBy(_.pc).foreach(state.visit) + state.result() + } + + private def combine(items: Vector[LuaPrototypeSemantics]): LuaPrototypeSemantics = + LuaPrototypeSemantics( + registerEvents = items.flatMap(_.registerEvents), + semanticSteps = items.flatMap(_.semanticSteps), + closureValues = items.flatMap(_.closureValues), + callSites = items.flatMap(_.callSites), + localFlows = items.flatMap(_.localFlows), + tableFieldFlows = items.flatMap(_.tableFieldFlows), + globalFlows = items.flatMap(_.globalFlows), + upvalueFlows = items.flatMap(_.upvalueFlows), + callTargetCandidates = items.flatMap(_.callTargetCandidates), + unresolvedCalls = items.flatMap(_.unresolvedCalls), + killOverwrites = items.flatMap(_.killOverwrites), + negativeExpectations = items.flatMap(_.negativeExpectations) + ) + + private def closureBindingsByPrototypeAndUpvalue(root: LuaPrototype): Map[String, UpvalueClosureBindings] = { + def collect( + parent: LuaPrototype, + currentUpvalueBindings: UpvalueClosureBindings + ): Map[String, UpvalueClosureBindings] = { + val directTargetsBySlot = scala.collection.mutable.Map.empty[Int, String] + val tableTargetsBySlot = scala.collection.mutable.Map.empty[Int, Map[String, String]] + val capturedByPrototype = scala.collection.mutable.Map.empty[String, UpvalueClosureBindings] + val sorted = parent.instructions.sortBy(_.pc) + val bindingPcs = closureBindingPcs(parent) + + def clearSlot(slot: Int): Unit = { + directTargetsBySlot -= slot + tableTargetsBySlot -= slot + } + + def keyName(value: Int): Option[String] = + if (value >= RkConstantBase) constantString(parent, value - RkConstantBase) else None + + def childUpvalueBindings(closure: LuaInstruction): UpvalueClosureBindings = { + val childPrototypeId = s"${parent.prototypeId}.${closure.b}" + parent.nested + .find(_.prototypeId == childPrototypeId) + .map { child => + val directTargets = sorted + .dropWhile(_.pc <= closure.pc) + .take(child.upvalueCount) + .zipWithIndex + .flatMap { + case (binder, upvalueSlot) if binder.opcode == LuaOpcode.Move => + directTargetsBySlot.get(binder.b).map(upvalueSlot -> _) + case (binder, upvalueSlot) if binder.opcode == LuaOpcode.GetUpval => + currentUpvalueBindings.directTargets.get(binder.b).map(upvalueSlot -> _) + case _ => None + } + .toMap + val tableTargets = sorted + .dropWhile(_.pc <= closure.pc) + .take(child.upvalueCount) + .zipWithIndex + .flatMap { + case (binder, upvalueSlot) if binder.opcode == LuaOpcode.Move => + tableTargetsBySlot.get(binder.b).map(upvalueSlot -> _) + case (binder, upvalueSlot) if binder.opcode == LuaOpcode.GetUpval => + currentUpvalueBindings.tableTargets.get(binder.b).map(upvalueSlot -> _) + case _ => None + } + .toMap + UpvalueClosureBindings(directTargets, tableTargets) + } + .getOrElse(UpvalueClosureBindings.Empty) + } + + sorted.foreach { + case instruction if bindingPcs(instruction.pc) => + case instruction if instruction.opcode == LuaOpcode.Closure => + clearSlot(instruction.a) + val targetPrototypeId = s"${parent.prototypeId}.${instruction.b}" + directTargetsBySlot += instruction.a -> targetPrototypeId + val childBindings = childUpvalueBindings(instruction) + if (childBindings.nonEmpty) { + capturedByPrototype += targetPrototypeId -> childBindings + } + parent.nested + .find(_.prototypeId == targetPrototypeId) + .foreach(child => capturedByPrototype ++= collect(child, childBindings)) + case instruction if instruction.opcode == LuaOpcode.Move => + val movedDirect = directTargetsBySlot.get(instruction.b) + val movedTable = tableTargetsBySlot.get(instruction.b) + clearSlot(instruction.a) + movedDirect.foreach(target => directTargetsBySlot += instruction.a -> target) + movedTable.foreach(targets => tableTargetsBySlot += instruction.a -> targets) + case instruction if instruction.opcode == LuaOpcode.GetUpval => + val inheritedDirect = currentUpvalueBindings.directTargets.get(instruction.b) + val inheritedTable = currentUpvalueBindings.tableTargets.get(instruction.b) + clearSlot(instruction.a) + inheritedDirect.foreach(target => directTargetsBySlot += instruction.a -> target) + inheritedTable.foreach(targets => tableTargetsBySlot += instruction.a -> targets) + case instruction if instruction.opcode == LuaOpcode.GetTable => + val loaded = for { + tableTargets <- tableTargetsBySlot.get(instruction.b) + key <- instruction.c.flatMap(keyName) + target <- tableTargets.get(key) + } yield target + clearSlot(instruction.a) + loaded.foreach(target => directTargetsBySlot += instruction.a -> target) + case instruction if instruction.opcode == LuaOpcode.SetTable => + for { + key <- keyName(instruction.b) + valueSlot <- instruction.c.flatMap(rkRegisterValue) + valueTarget <- directTargetsBySlot.get(valueSlot) + } { + val currentTargets = tableTargetsBySlot.getOrElse(instruction.a, Map.empty) + tableTargetsBySlot += instruction.a -> (currentTargets + (key -> valueTarget)) + } + case instruction if instruction.opcode == LuaOpcode.Call || instruction.opcode == LuaOpcode.TailCall => + clearSlot(instruction.a) + case instruction + if instruction.opcode == LuaOpcode.LoadK || instruction.opcode == LuaOpcode.LoadBool || + instruction.opcode == LuaOpcode.LoadNil || instruction.opcode == LuaOpcode.GetGlobal || + instruction.opcode == LuaOpcode.NewTable || + instruction.opcode == LuaOpcode.Self || instruction.opcode == LuaOpcode.Vararg => + clearSlot(instruction.a) + case _ => + } + + capturedByPrototype.toMap + } + + collect(root, UpvalueClosureBindings.Empty) + } + + private def constantString(prototype: LuaPrototype, index: Int): Option[String] = + prototype.constants.collectFirst { case LuaConstant(`index`, "string", LuaConstantValue.StringValue(value)) => + value + } + + private def rkRegisterValue(value: Int): Option[Int] = + if (value < RkConstantBase) Some(value) else None + + private def closureBindingPcs(prototype: LuaPrototype): Set[Int] = + prototype.instructions + .sortBy(_.pc) + .zipWithIndex + .flatMap { + case (closure, index) if closure.opcode == LuaOpcode.Closure => + prototype.nested.find(_.prototypeId == s"${prototype.prototypeId}.${closure.b}").toVector.flatMap { child => + (1 to child.upvalueCount) + .takeWhile { offset => + prototype.instructions + .sortBy(_.pc) + .lift(index + offset) + .exists(binding => + binding.pc == closure.pc + offset && + (binding.opcode == LuaOpcode.Move || binding.opcode == LuaOpcode.GetUpval) + ) + } + .flatMap(offset => prototype.instructions.sortBy(_.pc).lift(index + offset).map(_.pc)) + } + case _ => Vector.empty + } + .toSet + + private final class SemanticState(prototype: LuaPrototype, upvalueClosureBindings: UpvalueClosureBindings) { + private val registerEvents = Vector.newBuilder[LuaRegisterEvent] + private val semanticSteps = Vector.newBuilder[LuaSemanticStep] + private val closureValues = Vector.newBuilder[LuaClosureValue] + private val callSites = Vector.newBuilder[LuaCallSite] + private val localFlows = Vector.newBuilder[LuaLocalFlow] + private val tableFieldFlows = Vector.newBuilder[LuaTableFieldFlow] + private val globalFlows = Vector.newBuilder[LuaGlobalFlow] + private val upvalueFlows = Vector.newBuilder[LuaUpvalueFlow] + private val callTargetCandidates = Vector.newBuilder[LuaCallTargetCandidate] + private val unresolvedCalls = Vector.newBuilder[LuaUnresolvedCall] + private val killOverwrites = Vector.newBuilder[LuaKillOverwrite] + private val negativeExpectations = Vector.newBuilder[LuaNegativeExpectation] + + private var reaching = (0 until prototype.numParams).map(slot => slot -> Set(staticSlotRef(slot))).toMap + private var closuresBySlot = Map.empty[Int, LuaClosureValue] + private var tableObjectsBySlot = (0 until prototype.numParams).map(slot => slot -> staticSlotRef(slot)).toMap + private var tableWrites = Map.empty[(String, String), Set[String]] + private var closureTableWrites = Map.empty[(Int, String), LuaClosureValue] + private var globalWrites = Map.empty[String, Set[String]] + private var mutatedUpvalues = Set.empty[Int] + private var conditionalReachingAtPc = Map.empty[Int, Map[Int, Set[String]]] + private var controlPredicatesUntilPc = Vector.empty[(Int, Set[String])] + + def visit(instruction: LuaInstruction): Unit = { + controlPredicatesUntilPc = controlPredicatesUntilPc.filter { case (targetPc, _) => instruction.pc < targetPc } + conditionalReachingAtPc.get(instruction.pc).foreach { bypassReaching => + reaching = mergeReaching(reaching, bypassReaching) + conditionalReachingAtPc -= instruction.pc + } + instruction.opcode match { + case LuaOpcode.Move => + val source = readSlot(instruction, instruction.b) + val movedTableObject = tableObjectsBySlot.get(instruction.b) + val movedTableClosures = closureTableWrites.collect { + case ((tableSlot, key), closure) if tableSlot == instruction.b => key -> closure + } + writeSlot(instruction, instruction.a, Set(source), "move") + movedTableObject.foreach(tableObject => tableObjectsBySlot += instruction.a -> tableObject) + closuresBySlot.get(instruction.b).foreach { closure => + val moved = closure.copy(slot = instruction.a, valueRef = slotRef(instruction.pc, instruction.a)) + closuresBySlot += instruction.a -> moved + closureValues += moved + } + movedTableClosures.foreach { case (key, closure) => + closureTableWrites += (instruction.a, key) -> closure.copy( + slot = instruction.a, + valueRef = slotRef(instruction.pc, instruction.a) + ) + } + case LuaOpcode.LoadK => + writeSlot(instruction, instruction.a, Set(constantRef(instruction.b)), "loadk") + case LuaOpcode.NewTable => + writeSlot(instruction, instruction.a, Set(slotRef(instruction.pc, instruction.a)), "newtable") + tableObjectsBySlot += instruction.a -> slotRef(instruction.pc, instruction.a) + case LuaOpcode.LoadBool | LuaOpcode.LoadNil | LuaOpcode.Vararg => + writeSlot( + instruction, + instruction.a, + Set(slotRef(instruction.pc, instruction.a)), + instruction.opcode.mnemonic.toLowerCase + ) + case LuaOpcode.Closure => + val target = nestedPrototypeId(instruction.b) + val value = slotRef(instruction.pc, instruction.a) + writeSlot(instruction, instruction.a, Set(value), "closure") + val closure = LuaClosureValue(instruction.a, value, target, BytecodeProvenance) + closuresBySlot += instruction.a -> closure + closureValues += closure + case LuaOpcode.Call | LuaOpcode.TailCall => + handleCall(instruction) + case LuaOpcode.Concat => + handleConcat(instruction) + case LuaOpcode.GetUpval => + val read = slotRef(instruction.pc, instruction.a) + writeSlot(instruction, instruction.a, Set(read), "getupval") + upvalueClosureBindings.directTargets.get(instruction.b).foreach { targetPrototypeId => + val closure = LuaClosureValue(instruction.a, read, targetPrototypeId, BytecodeProvenance) + closuresBySlot += instruction.a -> closure + closureValues += closure + } + upvalueClosureBindings.tableTargets.get(instruction.b).foreach { targetsByKey => + targetsByKey.foreach { case (key, targetPrototypeId) => + closureTableWrites += (instruction.a, key) -> + LuaClosureValue(instruction.a, read, targetPrototypeId, BytecodeProvenance) + } + } + upvalueFlows += LuaUpvalueFlow(upvalueRef(instruction.b), read, read, read, BytecodeProvenance) + if (mutatedUpvalues(instruction.b)) { + addBoundary(UpvalueStaleBoundary, read, read, "upvalue mutation invalidates earlier read") + } + case LuaOpcode.SetUpval => + val read = readSlot(instruction, instruction.a) + mutatedUpvalues += instruction.b + addBoundary( + MutationBoundary, + read, + upvalueRef(instruction.b), + "SETUPVAL introduces an explicit mutation boundary" + ) + case LuaOpcode.GetGlobal => + val write = slotRef(instruction.pc, instruction.a) + writeSlot(instruction, instruction.a, Set(write), "getglobal") + stringConstant(instruction.b).foreach { name => + tableObjectsBySlot += instruction.a -> s"global:$name" + globalWrites.get(name).foreach { sources => + sources.foreach { source => + globalFlows += LuaGlobalFlow(name, source, write, source, BytecodeProvenance) + } + } + } + case LuaOpcode.SetGlobal => + val value = readSlot(instruction, instruction.a) + stringConstant(instruction.b).foreach(name => globalWrites += name -> Set(value)) + case LuaOpcode.GetTable => + handleGetTable(instruction) + case LuaOpcode.SetTable => + handleSetTable(instruction) + case LuaOpcode.SetList => + handleSetList(instruction) + case LuaOpcode.Self => + handleSelf(instruction) + case LuaOpcode.Return => + readReturnSlots(instruction).foreach(readSlot(instruction, _)) + case LuaOpcode.Eq | LuaOpcode.Lt | LuaOpcode.Le => + rkRegister(instruction.b).foreach(readSlot(instruction, _)) + instruction.c.flatMap(rkRegister).foreach(readSlot(instruction, _)) + case _ => + instruction.c.foreach { c => + rkRegister(instruction.b).foreach(readSlot(instruction, _)) + rkRegister(c).foreach(readSlot(instruction, _)) + } + } + conditionalForwardJumpTargetPc(instruction).foreach { target => + val existing = conditionalReachingAtPc.getOrElse(target, Map.empty) + conditionalReachingAtPc += target -> mergeReaching(existing, reaching) + previousInstruction(instruction).map(conditionalPredicateRefs).filter(_.nonEmpty).foreach { predicates => + controlPredicatesUntilPc :+= target -> predicates + } + } + } + + def result(): LuaPrototypeSemantics = { + LuaPrototypeSemantics( + registerEvents = registerEvents.result(), + semanticSteps = semanticSteps.result(), + closureValues = closureValues.result(), + callSites = callSites.result(), + localFlows = localFlows.result(), + tableFieldFlows = tableFieldFlows.result(), + globalFlows = globalFlows.result(), + upvalueFlows = upvalueFlows.result(), + callTargetCandidates = callTargetCandidates.result(), + unresolvedCalls = unresolvedCalls.result(), + killOverwrites = killOverwrites.result(), + negativeExpectations = negativeExpectations.result() + ) + } + + private def handleCall(instruction: LuaInstruction): Unit = { + val targetClosure = closuresBySlot.get(instruction.a) + val targetRead = readSlot(instruction, instruction.a) + val argSlots = callArgumentSlots(instruction) + val argumentReads = argSlots.map(readSlot(instruction, _)) + val returnSlots = callReturnSlots(instruction) + val callReads = (targetRead +: argumentReads).toSet + returnSlots.foreach { slot => + writeSlot(instruction, slot, callReads, "call-return") + } + val callsite = LuaCallSite( + callsiteId = instructionRef(instruction.pc), + prototypeId = prototype.prototypeId, + pc = instruction.pc, + opcode = instruction.opcode.mnemonic, + targetValueRef = targetRead, + firstArgSlot = argSlots.headOption, + argCount = Some(argSlots.size), + firstReturnSlot = returnSlots.headOption, + returnCount = Some(returnSlots.size) + ) + callSites += callsite + targetClosure match { + case Some(closure) => + callTargetCandidates += LuaCallTargetCandidate( + callsite.callsiteId, + closure.targetPrototypeId, + "candidate", + BytecodeProvenance + ) + case None => + if (isParamDerived(instruction.a)) { + unresolvedCalls += LuaUnresolvedCall(callsite.callsiteId, ParamDerivedReason, BoundaryProvenance) + } + } + } + + private def handleConcat(instruction: LuaInstruction): Unit = { + val c = requireC(instruction) + val sources = (instruction.b to c).map(slot => readSlot(instruction, slot)).toSet + writeSlot(instruction, instruction.a, sources, "concat") + } + + private def handleGetTable(instruction: LuaInstruction): Unit = { + val tableSlot = instruction.b + val tableObject = tableObjectsBySlot.get(tableSlot) + val tableRead = readSlot(instruction, tableSlot) + val keyReads = instruction.c.flatMap(rkRegister).map(readSlot(instruction, _)).toSet + val write = slotRef(instruction.pc, instruction.a) + val loadedClosure = instruction.c + .flatMap(rkConstantName) + .flatMap(key => closureTableWrites.get((tableSlot, key))) + writeSlot(instruction, instruction.a, keyReads + tableRead, "gettable") + instruction.c.flatMap(rkConstantRef).foreach { key => + tableObject.flatMap(identity => tableWrites.get((identity, key))).foreach { sources => + sources.foreach { source => + tableFieldFlows += LuaTableFieldFlow( + slotRef(instruction.pc, tableSlot), + key, + source, + write, + BytecodeProvenance + ) + } + } + } + loadedClosure.foreach { closure => + val loaded = closure.copy(slot = instruction.a, valueRef = write) + closuresBySlot += instruction.a -> loaded + closureValues += loaded + } + if (isGlobalEnvironmentTable(tableSlot)) { + instruction.c.flatMap(rkConstantName).foreach { name => + globalWrites.get(name).foreach { sources => + sources.foreach { source => + globalFlows += LuaGlobalFlow(name, source, write, source, BytecodeProvenance) + } + } + } + } + } + + private def handleSetTable(instruction: LuaInstruction): Unit = { + val tableSlot = instruction.a + readSlot(instruction, tableSlot) + rkRegister(instruction.b).foreach(readSlot(instruction, _)) + val valueSlot = instruction.c.flatMap(rkRegister) + val valueRefs = valueSlot.map(readSlot(instruction, _)).toSet + for { + tableObject <- tableObjectsBySlot.get(tableSlot) + key <- instruction.bOptionConstantString + } { + tableWrites += (tableObject, key) -> valueRefs + } + instruction.bOptionConstantName.foreach { key => + valueSlot.flatMap(closuresBySlot.get).foreach { closure => + closureTableWrites += (tableSlot, key) -> closure + } + } + if (isGlobalEnvironmentTable(tableSlot)) { + instruction.bOptionConstantName.foreach { name => + if (valueRefs.nonEmpty) { + globalWrites += name -> valueRefs + } + } + } + } + + private def handleSetList(instruction: LuaInstruction): Unit = { + val tableSlot = instruction.a + readSlot(instruction, tableSlot) + if (instruction.b > 0 && tableObjectsBySlot.contains(tableSlot)) { + val tableObject = tableObjectsBySlot(tableSlot) + val valueRefs = (1 to instruction.b).map(offset => readSlot(instruction, tableSlot + offset)) + instruction.c.foreach { block => + valueRefs.zipWithIndex.foreach { case (valueRef, index) => + tableWrites += (tableObject, setListElementKey(block, index)) -> Set(valueRef) + } + } + } + } + + private def handleSelf(instruction: LuaInstruction): Unit = { + readSlot(instruction, instruction.b) + instruction.c.flatMap(rkRegister).foreach(readSlot(instruction, _)) + writeSlot(instruction, instruction.a + 1, Set(slotRef(instruction.pc, instruction.b)), "self-base") + writeSlot(instruction, instruction.a, Set(slotRef(instruction.pc, instruction.a)), "self-member") + } + + private def readReturnSlots(instruction: LuaInstruction): Seq[Int] = + instruction.b match { + case 0 => Seq.empty + case 1 => Seq.empty + case n => instruction.a until (instruction.a + n - 1) + } + + private def callArgumentSlots(instruction: LuaInstruction): Seq[Int] = + instruction.b match { + case 0 => (instruction.a + 1) until prototype.maxStack + case 1 => Seq.empty + case n => (instruction.a + 1) until (instruction.a + n) + } + + private def callReturnSlots(instruction: LuaInstruction): Seq[Int] = + instruction.c match { + case Some(0) => Seq(instruction.a) + case Some(1) => Seq.empty + case Some(n) => instruction.a until (instruction.a + n - 1) + case None => Seq.empty + } + + private def readSlot(instruction: LuaInstruction, slot: Int): String = { + val read = slotRef(instruction.pc, slot) + registerEvents += LuaRegisterEvent("read", prototype.prototypeId, instruction.pc, slot, read) + reaching.get(slot).toSeq.flatten.foreach { source => + localFlows += LuaLocalFlow(source, read, "may-reaching-definition", BytecodeProvenance) + } + read + } + + private def writeSlot(instruction: LuaInstruction, slot: Int, sources: Set[String], kind: String): Unit = { + val write = slotRef(instruction.pc, slot) + registerEvents += LuaRegisterEvent("write", prototype.prototypeId, instruction.pc, slot, write) + sources.filterNot(_ == write).foreach { source => + localFlows += LuaLocalFlow(source, write, "same-instruction-dependence", BytecodeProvenance) + semanticSteps += LuaSemanticStep(source, write, kind) + } + controlPredicatesUntilPc.iterator.flatMap(_._2).toSet.filterNot(_ == write).foreach { predicate => + localFlows += LuaLocalFlow(predicate, write, "conditional-assignment-dependence", BytecodeProvenance) + } + reaching.get(slot).foreach { prior => + if (prior.nonEmpty && !prior.contains(write)) { + prior.foreach { first => + killOverwrites += LuaKillOverwrite( + s"${prototype.prototypeId}:pc${instruction.pc}:r$slot:kills:$first", + prototype.prototypeId, + first, + write, + write, + write, + "same-slot-overwrite-kills-prior-definition" + ) + } + } + } + reaching += slot -> Set(write) + closuresBySlot -= slot + tableObjectsBySlot -= slot + closureTableWrites = closureTableWrites.filterNot { case ((tableSlot, _), _) => tableSlot == slot } + } + + private def conditionalForwardJumpTargetPc(instruction: LuaInstruction): Option[Int] = + if (instruction.opcode == LuaOpcode.Jmp && previousInstruction(instruction).exists(isConditionalBranch)) { + val target = instruction.pc + 1 + instruction.b + Option.when(target > instruction.pc + 1)(target) + } else { + None + } + + private def previousInstruction(instruction: LuaInstruction): Option[LuaInstruction] = + prototype.instructions.find(_.pc == instruction.pc - 1) + + private def isConditionalBranch(instruction: LuaInstruction): Boolean = + Set(LuaOpcode.Eq, LuaOpcode.Lt, LuaOpcode.Le, LuaOpcode.Test, LuaOpcode.TestSet).contains(instruction.opcode) + + private def conditionalPredicateRefs(instruction: LuaInstruction): Set[String] = + instruction.opcode match { + case LuaOpcode.Eq | LuaOpcode.Lt | LuaOpcode.Le => + Vector(rkRegister(instruction.b), instruction.c.flatMap(rkRegister)).flatten + .map(slotRef(instruction.pc, _)) + .toSet + case LuaOpcode.Test => Set(slotRef(instruction.pc, instruction.a)) + case LuaOpcode.TestSet => Set(slotRef(instruction.pc, instruction.b)) + case _ => Set.empty + } + + private def mergeReaching(left: Map[Int, Set[String]], right: Map[Int, Set[String]]): Map[Int, Set[String]] = + (left.keySet ++ right.keySet).iterator.map { slot => + slot -> (left.getOrElse(slot, Set.empty) ++ right.getOrElse(slot, Set.empty)) + }.toMap + + private def isParamDerived(slot: Int): Boolean = + reachesParameter(reaching.getOrElse(slot, Set.empty), Set.empty) + + private def reachesParameter(refs: Set[String], seen: Set[String]): Boolean = { + val pending = refs.diff(seen) + pending.exists(_.startsWith(s"${prototype.prototypeId}:r")) || { + val parents = localFlows + .result() + .collect { + case flow if pending(flow.sinkRef) => flow.sourceRef + } + .toSet + parents.nonEmpty && reachesParameter(parents, seen ++ pending) + } + } + + private def isGlobalEnvironmentTable(slot: Int): Boolean = + reaching.get(slot).toSeq.flatten.exists { ref => + val globalGet = prototype.instructions.exists { instruction => + slotRef(instruction.pc, instruction.a) == ref && + instruction.opcode == LuaOpcode.GetGlobal && + stringConstant(instruction.b).contains("_G") + } + globalGet + } + + private def addBoundary(kind: String, sourceRef: String, sinkRef: String, reason: String): Unit = + negativeExpectations += LuaNegativeExpectation(s"$kind:$sourceRef->$sinkRef", sourceRef, sinkRef, kind, reason) + + private def slotRef(pc: Int, slot: Int): String = s"${prototype.prototypeId}@pc$pc:r$slot" + + private def instructionRef(pc: Int): String = s"${prototype.prototypeId}@pc$pc" + + private def staticSlotRef(slot: Int): String = s"${prototype.prototypeId}:r$slot" + + private def constantRef(index: Int): String = s"${prototype.prototypeId}:k$index" + + private def upvalueRef(index: Int): String = s"${prototype.prototypeId}:u$index" + + private def setListElementKey(block: Int, index: Int): String = s"${prototype.prototypeId}:setlist:$block:$index" + + private def nestedPrototypeId(ordinal: Int): String = s"${prototype.prototypeId}.$ordinal" + + private def stringConstant(index: Int): Option[String] = + prototype.constants.collectFirst { case LuaConstant(`index`, "string", LuaConstantValue.StringValue(value)) => + value + } + + private def rkRegister(value: Int): Option[Int] = + if (value < RkConstantBase) Some(value) else None + + private def rkConstantRef(value: Int): Option[String] = + if (value >= RkConstantBase) Some(constantRef(value - RkConstantBase)) else None + + private def rkConstantName(value: Int): Option[String] = + if (value >= RkConstantBase) stringConstant(value - RkConstantBase) else None + + private def requireC(instruction: LuaInstruction): Int = + instruction.c match { + case Some(value) => value + case None => + throw new IllegalArgumentException( + s"${instruction.opcode.mnemonic} at ${prototype.prototypeId}@pc${instruction.pc} has no C operand" + ) + } + + extension (instruction: LuaInstruction) { + private def bOptionConstantString: Option[String] = + if (instruction.b >= RkConstantBase) Some(constantRef(instruction.b - RkConstantBase)) else None + + private def bOptionConstantName: Option[String] = + if (instruction.b >= RkConstantBase) stringConstant(instruction.b - RkConstantBase) else None + } + + } +} diff --git a/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/bytecode/LuaProgramSemantics.scala b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/bytecode/LuaProgramSemantics.scala new file mode 100644 index 000000000000..b8bbc70300a7 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/bytecode/LuaProgramSemantics.scala @@ -0,0 +1,3554 @@ +package io.joern.lua2cpg.bytecode + +final case class LuaModuleResolution( + requireCallsiteId: String, + requireString: String, + resolutionStatus: String, + fromModulePath: String, + targetModulePath: Option[String], + unresolvedReason: Option[String], + provenance: String +) + +final case class LuaModuleReturnTable( + modulePath: String, + tableRef: String, + fieldName: String, + targetPrototypeId: String, + provenance: String +) + +final case class LuaModuleFieldCallTarget( + fromModulePath: String, + callsiteId: String, + fieldName: String, + targetModulePath: String, + targetPrototypeId: String, + provenance: String +) + +final case class LuaInterproceduralArgFlow( + callsiteId: String, + fromArgumentRef: String, + argumentIndex: Int, + targetModulePath: String, + targetPrototypeId: String, + toParameterRef: String, + provenance: String +) + +final case class LuaInterproceduralReturnFlow( + callsiteId: String, + targetModulePath: String, + targetPrototypeId: String, + calleeReturnRef: String, + callerResultRef: String, + provenance: String +) + +final case class LuaCrossBoundaryCallTarget( + fromModulePath: String, + callsiteId: String, + targetModulePath: String, + targetPrototypeId: String, + confidence: String, + provenance: String +) + +final case class LuaTaintPath( + sourceRef: String, + sinkRef: String, + pathSteps: Vector[String], + classification: String, + provenance: String +) + +final case class LuaE4Boundary(boundaryId: String, boundaryKind: String, reason: String) + +final case class LuaRuleMatch( + callsiteId: String, + ruleKind: String, + trigger: String, + matchedName: String, + parameterIndex: Option[Int], + provenance: String +) + +final case class LuaSourceEndpoint(sourceRef: String, callsiteId: String, trigger: String, provenance: String) + +final case class LuaSinkEndpoint( + sinkRef: String, + callsiteId: String, + trigger: String, + parameterIndex: Int, + provenance: String +) + +final case class LuaSanitizerCall( + callsiteId: String, + sanitizerName: String, + sanitizedValueRef: String, + provenance: String +) + +final case class LuaSanitizerClassification( + sourceRef: String, + sinkRef: String, + sanitizerCallsiteId: String, + sanitizerName: String, + appliesToSink: Boolean, + onDataflowChain: Boolean, + classification: String +) + +final case class LuaReportClassification(sourceRef: String, sinkRef: String, classification: String, reason: String) + +final case class LuaVulnerabilityReport( + sourceRef: String, + sinkRef: String, + pathStatus: String, + classification: String, + pathSteps: Vector[String], + provenance: String +) + +final case class LuaE5Boundary(boundaryId: String, boundaryKind: String, reason: String) + +final case class LuaProgramSemantics( + moduleResolutions: Vector[LuaModuleResolution], + moduleReturnTables: Vector[LuaModuleReturnTable], + moduleFieldCallTargets: Vector[LuaModuleFieldCallTarget], + interproceduralArgFlows: Vector[LuaInterproceduralArgFlow], + interproceduralReturnFlows: Vector[LuaInterproceduralReturnFlow], + crossBoundaryCallTargets: Vector[LuaCrossBoundaryCallTarget], + taintPaths: Vector[LuaTaintPath], + boundaries: Vector[LuaE4Boundary], + ruleMatches: Vector[LuaRuleMatch], + sourceEndpoints: Vector[LuaSourceEndpoint], + sinkEndpoints: Vector[LuaSinkEndpoint], + sanitizerCalls: Vector[LuaSanitizerCall], + sanitizerClassifications: Vector[LuaSanitizerClassification], + reportClassifications: Vector[LuaReportClassification], + vulnerabilityReports: Vector[LuaVulnerabilityReport], + e5Boundaries: Vector[LuaE5Boundary], + pathSearchStats: LuaPathSearchStats, + performanceAttribution: LuaPerformanceAttribution +) + +final case class LuaPairPerformanceProfile( + sourceRef: String, + sinkRef: String, + sourceCallsiteId: String, + sinkCallsiteId: String, + sourceTrigger: String, + sinkTrigger: String, + counters: Map[String, Long] +) { + val pairId: String = + s"$sourceRef|$sourceCallsiteId|$sourceTrigger->$sinkRef|$sinkCallsiteId|$sinkTrigger" +} + +final case class LuaPerformanceAttribution( + p1CandidateCount: Long, + p1RejectedCount: Long, + p1AcceptedCount: Long, + unattributedChangedFamilyWork: Long, + pairProfiles: Vector[LuaPairPerformanceProfile], + aggregateCounters: Map[String, Long] = Map.empty +) + +final case class LuaPathSearchStats( + localPathGraphModuleCount: Int, + localPathGraphBuildCount: Int, + localPathSearchCount: Int, + distinctLocalPathQueryCount: Int, + sourceSinkPairCount: Int, + qualifiedSourceSinkPairCount: Int, + prototypePrunedSourceSinkPairCount: Int +) + +object LuaProgramSemantics { + private val RkConstantBase = 256 + private val Provenance = "bytecode-only" + private val BoundaryProvenance = "bytecode-boundary" + private val RealFirmwareSanitizerSuffixes = Set( + "shellquote", + "tonumber", + "parseCmdline", + "_strformat", + "_cmdformat", + "macaddr", + "macFormat", + "ip4addr", + "injection_test", + "check_iface_name", + "includeQuote", + "checkTime", + "doShell", + "checkIp", + "includeXxs", + "filterExecShell", + "binaryBase64Enc", + "decCiphertext", + "sha256", + "setMacFilter", + "setIpFilter", + "apcli_get_connect", + "setWifiAPMode", + "cmdSafeCheck", + "checkLanIpMask", + "ipaddr", + "lan_wan_ip_conflict_chk", + "licenseActivated", + "is_activated", + "check_mac", + "set_mac_filter", + "encode", + "getDstRule", + "local_dev_data_check", + "stat", + "check_if_whitelist_opcode", + "param_safety_check", + "sqlite3_db_execute", + "getStorageMountPathByUuid", + "getWanIfname", + "hackCharsCheck", + "open", + "del", + "ip4mac", + "match", + "r29_0", + "r3_0", + "filePathGet", + "checkPort", + "setPTRules", + "apcli_get_ifname_form_band" + ) + private val PairCounterNames = Vector( + "source_reachability_check_count", + "source_reachability_accepted_count", + "prototype_unreachable_pair_count", + "source_specific_provenance_pruned_pair_count", + "parameter_position_check_count", + "parameter_position_accepted_count", + "parameter_position_pruned_count", + "path_constructor_check_count", + "path_constructor_accepted_count", + "path_constructor_pruned_count", + "bridge_argument_provenance_candidate_count", + "bridge_candidate_pc_pruned_count", + "bridge_candidate_reachability_pruned_count", + "bridge_local_path_attempt_count", + "bridge_local_path_success_count", + "local_path_search_count", + "distinct_local_path_query_count", + "local_path_cache_hit_count", + "local_path_cache_miss_count", + "local_path_graph_build_count", + "local_path_graph_cache_hit_count", + "local_path_graph_cache_miss_count", + "bridge_path_cache_hit_count", + "bridge_path_cache_miss_count", + "targeted_search_node_visit_count", + "targeted_search_edge_visit_count", + "early_candidate_short_circuit_count", + "taint_path_count", + "report_count" + ) + + private final class PerformanceAttributionCollector { + private final case class PairKey(source: LuaSourceEndpoint, sink: LuaSinkEndpoint) + private final case class ActivePair( + key: PairKey, + var counters: scala.collection.mutable.Map[String, Long], + var bridgeCandidates: Long = 0L, + var bridgePcPruned: Long = 0L, + var bridgeReachabilityPruned: Long = 0L, + var earlyShortCircuits: Long = 0L, + var producedPath: Boolean = false, + var pathAccepted: Long = 0L, + var pathPruned: Long = 0L + ) + private val profiles = + scala.collection.mutable.LinkedHashMap.empty[PairKey, scala.collection.mutable.Map[String, Long]] + private val aggregates = scala.collection.mutable.Map.empty[String, Long] + private var activePair: Option[ActivePair] = None + private var p1Candidates = 0L + private var p1Rejected = 0L + private var p1Accepted = 0L + private var unattributed = 0L + + def recordP1(accepted: Boolean): Unit = { + p1Candidates += 1 + if (accepted) p1Accepted += 1 else p1Rejected += 1 + } + + def withPair[A](source: LuaSourceEndpoint, sink: LuaSinkEndpoint)(work: => A): A = { + val previous = activePair + val key = PairKey(source, sink) + val current = ActivePair( + key, + scala.collection.mutable.Map( + "source_reachability_check_count" -> 1L, + "source_reachability_accepted_count" -> 1L, + "path_constructor_check_count" -> 1L, + "path_constructor_accepted_count" -> 1L, + "parameter_position_check_count" -> 1L, + "parameter_position_accepted_count" -> 1L + ) + ) + activePair = Some(current) + try work + finally { + val bridgeCandidates = current.counters.getOrElse("bridge_argument_provenance_candidate_count", 0L) + val shortCircuits = current.counters.getOrElse("early_candidate_short_circuit_count", 0L) + val localSearches = current.counters.getOrElse("local_path_search_count", 0L) + if (current.producedPath || (bridgeCandidates > shortCircuits && localSearches > 0L)) { + if (profiles.contains(key)) throw new IllegalStateException(s"duplicate attributed pair: ${pairId(key)}") + profiles += key -> current.counters + } + activePair = previous + } + } + + def increment(name: String, amount: Long = 1L): Unit = { + increment(aggregates, name, amount) + activePair match { + case Some(current) if current.counters != null => increment(current.counters, name, amount) + case Some(current) => incrementPending(current, name, amount) + case None => unattributed += amount + } + } + + def incrementAggregate(name: String, amount: Long = 1L): Unit = increment(aggregates, name, amount) + + def markPathProduced(): Unit = + activePair match { + case Some(current) => current.producedPath = true + case None => throw new IllegalStateException("path produced without active attributed pair") + } + + def materializeActivePair(): Unit = + activePair match { + case Some(current) if current.counters == null => + current.counters = scala.collection.mutable.Map( + "path_constructor_accepted_count" -> current.pathAccepted, + "path_constructor_pruned_count" -> current.pathPruned, + "bridge_argument_provenance_candidate_count" -> current.bridgeCandidates, + "bridge_candidate_pc_pruned_count" -> current.bridgePcPruned, + "bridge_candidate_reachability_pruned_count" -> current.bridgeReachabilityPruned, + "early_candidate_short_circuit_count" -> current.earlyShortCircuits + ) + case Some(_) => + case None => unattributed += 1 + } + + def incrementFor(source: LuaSourceEndpoint, sink: LuaSinkEndpoint, name: String): Unit = + increment( + profiles.getOrElse(PairKey(source, sink), throw new IllegalStateException("missing attributed pair")), + name, + 1L + ) + + def result(reports: Vector[LuaReportClassification]): LuaPerformanceAttribution = { + profiles.foreach { case (key, counters) => + val reportCount = + reports.count(report => report.sourceRef == key.source.sourceRef && report.sinkRef == key.sink.sinkRef) + if (reportCount > 0) increment(counters, "report_count", reportCount.toLong) + } + val rows = profiles.toVector.map { case (key, values) => + LuaPairPerformanceProfile( + key.source.sourceRef, + key.sink.sinkRef, + scopedCallsite(key.source.callsiteId), + scopedCallsite(key.sink.callsiteId), + key.source.trigger, + key.sink.trigger, + PairCounterNames.map(name => name -> values.getOrElse(name, 0L)).toMap + ) + } + LuaPerformanceAttribution( + p1Candidates, + p1Rejected, + p1Accepted, + unattributed, + rows, + PairCounterNames.map(name => name -> aggregates.getOrElse(name, 0L)).toMap + ) + } + + private def increment(values: scala.collection.mutable.Map[String, Long], name: String, amount: Long): Unit = + values.update(name, values.getOrElse(name, 0L) + amount) + + private def incrementPending(current: ActivePair, name: String, amount: Long): Unit = + name match { + case "path_constructor_check_count" => + case "path_constructor_accepted_count" => current.pathAccepted += amount + case "path_constructor_pruned_count" => current.pathPruned += amount + case "bridge_argument_provenance_candidate_count" => current.bridgeCandidates += amount + case "bridge_candidate_pc_pruned_count" => current.bridgePcPruned += amount + case "bridge_candidate_reachability_pruned_count" => current.bridgeReachabilityPruned += amount + case "early_candidate_short_circuit_count" => current.earlyShortCircuits += amount + case _ => unattributed += amount + } + + private def scopedCallsite(ref: String): String = { + val split = parseQualifiedValueRef(ref) + s"${split.modulePath}::${split.localRef.split(":r", 2).head}" + } + + private def pairId(key: PairKey): String = + s"${key.source.sourceRef}|${key.source.callsiteId}|${key.source.trigger}->${key.sink.sinkRef}|${key.sink.callsiteId}|${key.sink.trigger}" + } + + def normalize(artifacts: Vector[(String, LuaBytecodeDecodeResult)]): LuaProgramSemantics = { + val attribution = new PerformanceAttributionCollector + val accepted = artifacts.collect { + case (path, result) if result.artifact.accepted => + ProgramArtifact(path, result.root) + } + val modules = accepted.flatMap(ModuleSummary.fromArtifact) + val moduleIndex = new ModuleIndex(modules) + val resolutions = modules.flatMap(module => module.requireCalls.map(resolveRequire(module, _, moduleIndex))) + val returnTables = modules.flatMap(_.exports.map { moduleExport => + LuaModuleReturnTable( + moduleExport.modulePath, + moduleExport.tableRef, + moduleExport.fieldName, + moduleExport.targetPrototypeId, + Provenance + ) + }) + val fieldTargets = + modules.flatMap(module => module.fieldCalls.flatMap(resolveFieldCall(module, _, resolutions, moduleIndex))) + val crossTargets = fieldTargets.map(target => + LuaCrossBoundaryCallTarget( + target.fromModulePath, + target.callsiteId, + target.targetModulePath, + target.targetPrototypeId, + "candidate", + Provenance + ) + ) + val localArgFlows = modules.flatMap(localInterproceduralArgFlows) + val localReturnFlows = modules.flatMap(localInterproceduralReturnFlows) + val crossInterprocedural = fieldTargets.flatMap(target => crossModuleFlows(target, moduleIndex)) + val sourceEndpoints = modules.flatMap(sourceEndpointsForModule).distinct + val sinkEndpoints = modules.flatMap(sinkEndpointsForModule(_, attribution)).distinct + val allArgFlows = (localArgFlows ++ crossInterprocedural.map(_._1)).distinct + val allReturnFlows = (localReturnFlows ++ crossInterprocedural.flatMap(_._2)).distinct + val pathSearch = new LocalPathSearch(modules, attribution) + val sourceSinkPruning = + new SourceSinkPruning(modules, fieldTargets, allArgFlows, allReturnFlows, sinkEndpoints, attribution) + val qualifiedSinksBySource = sourceEndpoints.map { sourceEndpoint => + sourceEndpoint -> sourceSinkPruning.qualifiedSinkEndpoints(sourceEndpoint, sinkEndpoints) + }.toMap + val taintPaths = + realFirmwareTaintPaths( + modules, + sourceEndpoints, + qualifiedSinksBySource, + allArgFlows, + allReturnFlows, + pathSearch, + attribution + ) + val boundaries = semanticBoundaries(modules, resolutions, fieldTargets) + val ruleMatches = ruleMatchesFor(sourceEndpoints, sinkEndpoints) + val sanitizerCalls = sanitizerCallsFor(modules) + val sanitizerClassifications = sanitizerClassificationsFor(taintPaths, sanitizerCalls) + val reportClassifications = reportClassificationsFor(taintPaths, sanitizerClassifications) + val vulnerabilityReports = vulnerabilityReportsFor(taintPaths, reportClassifications) + val e5Boundaries = Vector.empty[LuaE5Boundary] + + LuaProgramSemantics( + moduleResolutions = resolutions, + moduleReturnTables = returnTables, + moduleFieldCallTargets = fieldTargets, + interproceduralArgFlows = allArgFlows, + interproceduralReturnFlows = allReturnFlows, + crossBoundaryCallTargets = crossTargets.distinct, + taintPaths = taintPaths.distinct, + boundaries = boundaries.distinct, + ruleMatches = ruleMatches.distinct, + sourceEndpoints = sourceEndpoints.distinct, + sinkEndpoints = sinkEndpoints.distinct, + sanitizerCalls = sanitizerCalls.distinct, + sanitizerClassifications = sanitizerClassifications.distinct, + reportClassifications = reportClassifications.distinct, + vulnerabilityReports = vulnerabilityReports.distinct, + e5Boundaries = e5Boundaries.distinct, + pathSearchStats = pathSearch.stats( + sourceSinkPairCount = sourceEndpoints.size * sinkEndpoints.size, + qualifiedSourceSinkPairCount = qualifiedSinksBySource.values.map(_.size).sum + ), + performanceAttribution = attribution.result(reportClassifications) + ) + } + + private def resolveRequire( + module: ModuleSummary, + requireCall: RequireCall, + moduleIndex: ModuleIndex + ): LuaModuleResolution = + requireCall.requireString match { + case Some(name) => + moduleIndex.resolve(name) match { + case ModuleResolutionResult.Matched(target) => + LuaModuleResolution( + requireCall.callsiteId, + name, + "matched", + module.path, + Some(target.path), + None, + Provenance + ) + case ModuleResolutionResult.Unresolved => + LuaModuleResolution( + requireCall.callsiteId, + name, + "unresolved", + module.path, + None, + Some("unresolved-module"), + BoundaryProvenance + ) + } + case None => + LuaModuleResolution( + requireCall.callsiteId, + "dynamic", + "dynamic", + module.path, + None, + Some("dynamic-require"), + BoundaryProvenance + ) + } + + private def resolveFieldCall( + module: ModuleSummary, + call: FieldCall, + resolutions: Vector[LuaModuleResolution], + moduleIndex: ModuleIndex + ): Option[LuaModuleFieldCallTarget] = { + val targetModulePath = resolutions + .filter(resolution => resolution.fromModulePath == module.path && resolution.resolutionStatus == "matched") + .find(resolution => call.requireRef.exists(ref => requireResolutionReturns(module, resolution, ref))) + .flatMap(_.targetModulePath) + targetModulePath.flatMap { path => + moduleIndex + .module(path) + .flatMap(_.exports.find(_.fieldName == call.fieldName)) + .map(moduleExport => + LuaModuleFieldCallTarget( + module.path, + call.callsiteId, + call.fieldName, + path, + moduleExport.targetPrototypeId, + Provenance + ) + ) + } + } + + private def requireResolutionReturns( + module: ModuleSummary, + resolution: LuaModuleResolution, + requireRef: String + ): Boolean = + module.requireCalls.exists(call => + call.callsiteId == resolution.requireCallsiteId && + call.resultRef.contains(requireRef) + ) + + private def localInterproceduralArgFlows(module: ModuleSummary): Vector[LuaInterproceduralArgFlow] = + module.localCalls.flatMap { call => + module.prototype(call.targetPrototypeId).toVector.flatMap { callee => + for { + (fromArg, argumentIndex) <- call.argumentRefs.zipWithIndex + toParam <- callee.parameterRefs.lift(argumentIndex) + if !parameterFlowsToCallTarget(callee, argumentIndex) + } yield LuaInterproceduralArgFlow( + call.callsiteId, + qualify(module.path, fromArg), + argumentIndex, + module.path, + call.targetPrototypeId, + qualify(module.path, toParam), + Provenance + ) + } + } + + private def localInterproceduralReturnFlows(module: ModuleSummary): Vector[LuaInterproceduralReturnFlow] = + module.localCalls.flatMap { call => + module.prototype(call.targetPrototypeId).flatMap { callee => + for { + calleeReturn <- callee.returnRefs.headOption + callerResult <- call.resultRef + } yield LuaInterproceduralReturnFlow( + call.callsiteId, + module.path, + call.targetPrototypeId, + calleeReturn, + qualify(module.path, callerResult), + Provenance + ) + } + } + + private def crossModuleFlows( + target: LuaModuleFieldCallTarget, + moduleIndex: ModuleIndex + ): Vector[(LuaInterproceduralArgFlow, Option[LuaInterproceduralReturnFlow])] = { + val module = moduleIndex + .module(target.fromModulePath) + .getOrElse( + throw new IllegalStateException( + s"missing source module for resolved field target: module=${target.fromModulePath}" + ) + ) + val call = module.fieldCalls + .find(_.callsiteId == target.callsiteId) + .getOrElse( + throw new IllegalStateException( + s"missing field call for resolved field target: module=${target.fromModulePath} callsite=${target.callsiteId}" + ) + ) + val callee = moduleIndex + .module(target.targetModulePath) + .flatMap(_.prototype(target.targetPrototypeId)) + .getOrElse( + throw new IllegalStateException( + "missing callee prototype for resolved field target: " + + s"module=${target.targetModulePath} prototype=${target.targetPrototypeId}" + ) + ) + + val returnFlow = for { + calleeReturn <- callee.returnRefs.headOption + callerResult <- call.resultRef + } yield LuaInterproceduralReturnFlow( + target.callsiteId, + target.targetModulePath, + target.targetPrototypeId, + calleeReturn, + qualify(module.path, callerResult), + Provenance + ) + call.argumentRefs.zipWithIndex.flatMap { case (fromArg, argumentIndex) => + callee.parameterRefs + .lift(argumentIndex) + .toVector + .filter(_ => !parameterFlowsToCallTarget(callee, argumentIndex)) + .map { toParam => + LuaInterproceduralArgFlow( + target.callsiteId, + qualify(module.path, fromArg), + argumentIndex, + target.targetModulePath, + target.targetPrototypeId, + qualify(target.targetModulePath, toParam), + Provenance + ) -> returnFlow + } + } + } + + private def realFirmwareTaintPaths( + modules: Vector[ModuleSummary], + sourceEndpoints: Vector[LuaSourceEndpoint], + qualifiedSinksBySource: Map[LuaSourceEndpoint, Vector[LuaSinkEndpoint]], + interproceduralArgFlows: Vector[LuaInterproceduralArgFlow], + interproceduralReturnFlows: Vector[LuaInterproceduralReturnFlow], + pathSearch: LocalPathSearch, + attribution: PerformanceAttributionCollector + ): Vector[LuaTaintPath] = { + val crossModuleBridgeIndex = + new CrossModuleBridgeIndex( + interproceduralArgFlows, + interproceduralReturnFlows, + realFirmwareSanitizerProducedRefs(modules), + attribution + ) + sourceEndpoints.flatMap { source => + val qualifiedSinks = qualifiedSinksBySource + .get(source) + .getOrElse(throw new IllegalStateException(s"missing qualified sink set for source ${source.sourceRef}")) + qualifiedSinks.flatMap { sink => + val path = attribution.withPair(source, sink) { + val result = semanticBridgePath(source, sink, crossModuleBridgeIndex, pathSearch) + .orElse(samePrototypeForwardPath(pathSearch, source.sourceRef, sink.sinkRef)) + if (result.nonEmpty) attribution.markPathProduced() + result + } + path.map { pathSteps => + attribution.incrementFor(source, sink, "taint_path_count") + LuaTaintPath(source.sourceRef, sink.sinkRef, pathSteps, "true-positive", Provenance) + } + } + }.distinct + } + + private def samePrototypeForwardPath( + pathSearch: LocalPathSearch, + sourceRef: String, + sinkRef: String + ): Option[Vector[String]] = { + val source = parseQualifiedValueRef(sourceRef) + val sink = parseQualifiedValueRef(sinkRef) + if ( + source.modulePath == sink.modulePath && + source.prototypeId == sink.prototypeId && + source.pc <= sink.pc + ) + pathSearch.moduleLocalPathMode( + sourceRef, + sinkRef, + includeRepresentativeCallReturns = true, + includeRepresentativeValueEdges = true + ) + else None + } + + private def semanticBridgePath( + sourceEndpoint: LuaSourceEndpoint, + sinkEndpoint: LuaSinkEndpoint, + crossModuleBridgeIndex: CrossModuleBridgeIndex, + pathSearch: LocalPathSearch + ): Option[Vector[String]] = { + val provenanceBridge = crossModuleBridgeIndex.sourceScopedBridgePath( + sourceEndpoint, + sinkEndpoint, + pathSearch, + includeRepresentativeValueEdges = true + ) + if ( + provenanceBridge + .exists(path => !crossModuleBridgeIndex.pathCrossesSourceSanitizerProducedRef(path, sourceEndpoint.sourceRef)) + ) { + provenanceBridge + } else { + val needsArgumentReturnBridge = + crossModuleBridgeIndex.sourceRequiresRepresentativeReturnBridge(sourceEndpoint, sinkEndpoint, pathSearch) + val argumentReturnBridge = + if (provenanceBridge.nonEmpty || needsArgumentReturnBridge) { + crossModuleBridgeIndex.sourceScopedRepresentativeBridgePath(sourceEndpoint, sinkEndpoint, pathSearch) + } else { + None + } + selectUnsanitizedAlternative(provenanceBridge, argumentReturnBridge, sourceEndpoint, crossModuleBridgeIndex) + } + } + + private def selectUnsanitizedAlternative( + preferredPath: Option[Vector[String]], + alternativePath: Option[Vector[String]], + sourceEndpoint: LuaSourceEndpoint, + crossModuleBridgeIndex: CrossModuleBridgeIndex + ): Option[Vector[String]] = + (preferredPath, alternativePath) match { + case (Some(preferred), Some(alternative)) + if crossModuleBridgeIndex.pathCrossesSourceSanitizerProducedRef(preferred, sourceEndpoint.sourceRef) && + !crossModuleBridgeIndex.pathCrossesSourceSanitizerProducedRef(alternative, sourceEndpoint.sourceRef) => + Some(alternative) + case (Some(preferred), _) => Some(preferred) + case (None, alternative) => alternative + } + + private def representativeSinkSeeds(module: ModuleSummary, sinkEndpoint: LuaSinkEndpoint): Vector[String] = { + val sink = parseQualifiedValueRef(sinkEndpoint.sinkRef) + module + .prototype(sink.prototypeId) + .map { prototype => + val sanitizerReturn = + prototype.calls + .filter(call => call.pc < sink.pc && call.returnRefs.nonEmpty) + .filter(call => isRepresentativeSanitizerCall(call.resolvedName)) + .sortBy(_.pc) + .flatMap(_.returnRefs.headOption) + .map(qualify(module.path, _)) + + val sinkSlot = slotFromValueRef(sinkEndpoint.sinkRef) + val slotSeeds = prototype.instructions + .filter(instruction => instruction.pc < sink.pc && instruction.a == sinkSlot) + .sortBy(_.pc) + .map(instruction => qualify(module.path, s"${prototype.prototypeId}@pc${instruction.pc}:r${instruction.a}")) + + (sanitizerReturn ++ slotSeeds).distinct + } + .getOrElse(Vector.empty) + } + + private final class CrossModuleBridgeIndex( + interproceduralArgFlows: Vector[LuaInterproceduralArgFlow], + interproceduralReturnFlows: Vector[LuaInterproceduralReturnFlow], + sanitizerProducedRefs: Set[String], + attribution: PerformanceAttributionCollector + ) { + private final case class BridgeFlow( + sourcePrototype: String, + targetPrototype: String, + fromRef: String, + toRef: String, + callsiteId: String, + callsitePc: Int, + sortIndex: Int, + isArgumentFlow: Boolean + ) + + private val argumentBridgeFlows: Vector[BridgeFlow] = interproceduralArgFlows.map { flow => + val source = parseQualifiedValueRef(flow.fromArgumentRef) + BridgeFlow( + prototypeRef(source.modulePath, source.prototypeId), + prototypeRef(flow.targetModulePath, flow.targetPrototypeId), + flow.fromArgumentRef, + flow.toParameterRef, + flow.callsiteId, + requiredCallsitePc(flow.callsiteId), + flow.argumentIndex, + isArgumentFlow = true + ) + } + private val returnBridgeFlows: Vector[BridgeFlow] = interproceduralReturnFlows.map { flow => + val caller = parseQualifiedValueRef(flow.callerResultRef) + BridgeFlow( + prototypeRef(flow.targetModulePath, flow.targetPrototypeId), + prototypeRef(caller.modulePath, caller.prototypeId), + qualify(flow.targetModulePath, flow.calleeReturnRef), + flow.callerResultRef, + flow.callsiteId, + requiredCallsitePc(flow.callsiteId), + sortIndex = 0, + isArgumentFlow = false + ) + } + private val representativeBridgeFlows = (argumentBridgeFlows ++ returnBridgeFlows).distinct + private val directEdges: Set[(String, String)] = + argumentBridgeFlows.map(flow => flow.sourcePrototype -> flow.targetPrototype).toSet + private val representativeDirectEdges: Set[(String, String)] = + representativeBridgeFlows.map(flow => flow.sourcePrototype -> flow.targetPrototype).toSet + private val outgoingPrototypeEdges: Map[String, Set[String]] = + directEdges.groupMap(_._1)(_._2).view.mapValues(_.toSet).toMap + private val representativeIncomingPrototypeEdges: Map[String, Set[String]] = + representativeDirectEdges.groupMap(_._2)(_._1).view.mapValues(_.toSet).toMap + private val reachabilityCache = scala.collection.mutable.Map.empty[(String, String), Boolean] + private val representativeDistanceCache = scala.collection.mutable.Map.empty[String, Map[String, Int]] + private val representativeValueReachabilityCache = scala.collection.mutable.Map.empty[String, Set[String]] + private val localBridgePathCache = + scala.collection.mutable.Map.empty[(String, String, Boolean, Boolean), Option[Vector[String]]] + + private val argumentFlowsBySourcePrototype: Map[String, Vector[BridgeFlow]] = + argumentBridgeFlows.groupBy(_.sourcePrototype) + private val representativeFlowsBySourcePrototype: Map[String, Vector[BridgeFlow]] = + representativeBridgeFlows.groupBy(_.sourcePrototype) + + def sourceScopedBridgePath( + sourceEndpoint: LuaSourceEndpoint, + sinkEndpoint: LuaSinkEndpoint, + pathSearch: LocalPathSearch, + includeRepresentativeValueEdges: Boolean + ): Option[Vector[String]] = { + val source = parseQualifiedValueRef(sourceEndpoint.sourceRef) + val sink = parseQualifiedValueRef(sinkEndpoint.sinkRef) + val sinkPrototype = prototypeRef(sink.modulePath, sink.prototypeId) + val pending = scala.collection.mutable.Queue((sourceEndpoint.sourceRef, Vector(sourceEndpoint.sourceRef), 0)) + val seen = scala.collection.mutable.Set(sourceEndpoint.sourceRef) + val maxDepth = 4 + var firstSanitizedPath: Option[Vector[String]] = None + var unsanitizedPath: Option[Vector[String]] = None + + while (pending.nonEmpty && unsanitizedPath.isEmpty) { + val (currentRef, pathPrefix, depth) = pending.dequeue() + val currentPrototype = prototypeRefFromAnyQualifiedRef(currentRef) + val currentPc = pcFromAnyValueRef(currentRef) + + val currentIsEntryParameter = isPrototypeEntryParameterRef(currentRef) + val candidateFlows = argumentFlowsBySourcePrototype + .getOrElse(currentPrototype, Vector.empty) + .filter { flow => + attribution.increment("bridge_argument_provenance_candidate_count") + val flowPc = flow.callsitePc + val accepted = currentPc match { + case Some(pc) if flowPc >= pc => true + case Some(_) => false + case None if currentIsEntryParameter => true + case None => + throw new IllegalStateException( + s"missing pc provenance for cross-module bridge ref: current_ref=$currentRef" + ) + } + if (!accepted) { + attribution.increment("bridge_candidate_pc_pruned_count") + attribution.increment("early_candidate_short_circuit_count") + } + accepted + } + .filter { flow => + val accepted = + flow.targetPrototype == sinkPrototype || reachesSinkPrototype(flow.targetPrototype, sinkPrototype) + if (!accepted) { + attribution.increment("bridge_candidate_reachability_pruned_count") + attribution.increment("early_candidate_short_circuit_count") + } + accepted + } + .sortBy(flow => (flow.callsitePc, flow.targetPrototype, flow.sortIndex)) + + val reachableArgumentPaths = { + val candidateRefs = candidateFlows.map(_.fromRef).toSet + if (includeRepresentativeValueEdges) + pathSearch.moduleLocalPathsToAnyWithRepresentativeValues(currentRef, candidateRefs) + else + pathSearch.moduleLocalPathsToAny(currentRef, candidateRefs, includeRepresentativeCallReturns = true) + } + + val candidateIterator = candidateFlows.filter(flow => reachableArgumentPaths.contains(flow.fromRef)).iterator + while (candidateIterator.hasNext && unsanitizedPath.isEmpty) { + val flow = candidateIterator.next() + attribution.materializeActivePair() + val targetParamRef = flow.toRef + val targetPrototype = flow.targetPrototype + val currentToArgumentPath = preferredLocalPathMode( + pathSearch, + currentRef, + flow.fromRef, + includeRepresentativeCallReturns = true, + includeRepresentativeValueEdges = includeRepresentativeValueEdges + ) + currentToArgumentPath + .foreach { currentToArgument => + val bridgePrefix = pathPrefix ++ currentToArgument.drop(1) :+ targetParamRef + if (targetPrototype == sinkPrototype) { + preferredLocalPathMode( + pathSearch, + targetParamRef, + sinkEndpoint.sinkRef, + includeRepresentativeCallReturns = true, + includeRepresentativeValueEdges = includeRepresentativeValueEdges + ) + .map(parameterToSink => bridgePrefix ++ parameterToSink.drop(1)) + .foreach { completePath => + if (crossesSourceSanitizerProducedRef(completePath, sourceEndpoint.sourceRef)) { + if (firstSanitizedPath.isEmpty) firstSanitizedPath = Some(completePath) + } else { + unsanitizedPath = Some(completePath) + } + } + } else if (depth < maxDepth && !seen(targetParamRef)) { + seen += targetParamRef + pending.enqueue((targetParamRef, bridgePrefix, depth + 1)) + } + } + } + + } + + unsanitizedPath.orElse(firstSanitizedPath) + } + + def sourceScopedRepresentativeBridgePath( + sourceEndpoint: LuaSourceEndpoint, + sinkEndpoint: LuaSinkEndpoint, + pathSearch: LocalPathSearch + ): Option[Vector[String]] = { + val source = parseQualifiedValueRef(sourceEndpoint.sourceRef) + val sink = parseQualifiedValueRef(sinkEndpoint.sinkRef) + val sourcePrototype = prototypeRef(source.modulePath, source.prototypeId) + val sinkPrototype = prototypeRef(sink.modulePath, sink.prototypeId) + val distanceToSink = representativeDistancesToSink(sinkPrototype) + val pending = + scala.collection.mutable.Queue( + (sourceEndpoint.sourceRef, Vector(sourceEndpoint.sourceRef), sourcePrototype, List.empty[String], 0) + ) + val seen = scala.collection.mutable.Set((sourceEndpoint.sourceRef, List.empty[String])) + val maxDepth = 4 + var found: Option[Vector[String]] = None + + while (pending.nonEmpty && found.isEmpty) { + val (currentRef, pathPrefix, currentPrototype, callContext, depth) = pending.dequeue() + val currentPc = pcFromAnyValueRef(currentRef) + val currentIsEntryParameter = isPrototypeEntryParameterRef(currentRef) + val currentDistance = distanceToSink.getOrElse(currentPrototype, Int.MaxValue) + val candidateFlows = + (if (depth == 0) argumentFlowsBySourcePrototype else representativeFlowsBySourcePrototype) + .getOrElse(currentPrototype, Vector.empty) + .flatMap(flow => advanceCallContext(flow, callContext).map(flow -> _)) + .filter { flow => + attribution.increment("bridge_argument_provenance_candidate_count") + val pcAccepted = + if (depth == 0) flow._1.callsitePc > source.pc + else if (!flow._1.isArgumentFlow) true + else { + val flowPc = flow._1.callsitePc + currentPc match { + case Some(pc) if flowPc >= pc => true + case Some(_) => false + case None if currentIsEntryParameter => true + case None => + throw new IllegalStateException( + s"missing pc provenance for cross-module bridge ref: current_ref=$currentRef" + ) + } + } + if (!pcAccepted) { + attribution.increment("bridge_candidate_pc_pruned_count") + attribution.increment("early_candidate_short_circuit_count") + } + pcAccepted + } + .filter { flow => + val reachable = + if (depth == 0 && sourcePrototype == sinkPrototype) + flow._1.targetPrototype != sinkPrototype && distanceToSink.contains(flow._1.targetPrototype) + else distanceToSink.get(flow._1.targetPrototype).contains(currentDistance - 1) + if (!reachable) { + attribution.increment("bridge_candidate_reachability_pruned_count") + attribution.increment("early_candidate_short_circuit_count") + } + reachable + } + .sortBy { case (flow, _) => (flow.callsitePc, flow.targetPrototype, flow.sortIndex) } + + val reachableBridgePrefixes = + reachableRepresentativeBridgePrefixes( + pathSearch, + currentRef, + candidateFlows.map(_._1), + includeArgumentRepresentativeValues = depth > 0 + ) + + val candidateIterator = candidateFlows.filter { case (flow, _) => + reachableBridgePrefixes.contains(flow.fromRef) + }.iterator + while (candidateIterator.hasNext && found.isEmpty) { + val (flow, nextCallContext) = candidateIterator.next() + val bridgePrefix = pathPrefix ++ reachableBridgePrefixes(flow.fromRef).drop(1) :+ flow.toRef + if (flow.targetPrototype == sinkPrototype) { + found = preferredLocalPathForBridgeFlow(pathSearch, flow, sinkEndpoint.sinkRef) + .map(targetToSink => bridgePrefix ++ targetToSink.drop(1)) + } else if (depth < maxDepth && !seen((flow.toRef, nextCallContext))) { + seen += ((flow.toRef, nextCallContext)) + pending.enqueue((flow.toRef, bridgePrefix, flow.targetPrototype, nextCallContext, depth + 1)) + } + } + } + + found + } + + private def advanceCallContext(flow: BridgeFlow, callContext: List[String]): Option[List[String]] = + if (flow.isArgumentFlow) Some(flow.callsiteId :: callContext) + else + callContext match { + case callsiteId :: tail if callsiteId == flow.callsiteId => Some(tail) + case Nil => Some(Nil) + case _ => None + } + + private def reachableRepresentativeBridgePrefixes( + pathSearch: LocalPathSearch, + currentRef: String, + candidateFlows: Vector[BridgeFlow], + includeArgumentRepresentativeValues: Boolean + ): Map[String, Vector[String]] = { + val argumentFlowRefs = candidateFlows.filter(_.isArgumentFlow).map(_.fromRef).toSet + val argumentFlowPaths = + if (includeArgumentRepresentativeValues) + pathSearch.moduleLocalPathsToAnyWithRepresentativeValues(currentRef, argumentFlowRefs) + else + pathSearch.moduleLocalPathsToAny(currentRef, argumentFlowRefs, includeRepresentativeCallReturns = true) + val returnFlowPaths = pathSearch.moduleLocalPathsToAny( + currentRef, + candidateFlows.filterNot(_.isArgumentFlow).map(_.fromRef).toSet, + includeRepresentativeCallReturns = true + ) + argumentFlowPaths ++ returnFlowPaths + } + + private def representativeDistancesToSink(sinkPrototype: String): Map[String, Int] = + representativeDistanceCache.getOrElseUpdate( + sinkPrototype, { + val pending = scala.collection.mutable.Queue(sinkPrototype) + val distance = scala.collection.mutable.Map(sinkPrototype -> 0) + while (pending.nonEmpty) { + val current = pending.dequeue() + val currentDistance = distance(current) + representativeIncomingPrototypeEdges.getOrElse(current, Set.empty).foreach { previous => + if (!distance.contains(previous)) { + distance(previous) = currentDistance + 1 + pending.enqueue(previous) + } + } + } + distance.toMap + } + ) + + def sourceRequiresRepresentativeReturnBridge( + sourceEndpoint: LuaSourceEndpoint, + sinkEndpoint: LuaSinkEndpoint, + pathSearch: LocalPathSearch + ): Boolean = { + val sinkPrototype = prototypeRef(sinkEndpoint.sinkRef) + representativeValueReachableReturnPrototypes(sourceEndpoint, pathSearch)(sinkPrototype) + } + + private def representativeValueReachableReturnPrototypes( + sourceEndpoint: LuaSourceEndpoint, + pathSearch: LocalPathSearch + ): Set[String] = + representativeValueReachabilityCache.getOrElseUpdate( + sourceEndpoint.sourceRef, { + val source = parseQualifiedValueRef(sourceEndpoint.sourceRef) + val sourcePrototype = prototypeRef(source.modulePath, source.prototypeId) + val pending = + scala.collection.mutable.Queue((sourceEndpoint.sourceRef, sourcePrototype, List.empty[String], 0, false)) + val seen = + scala.collection.mutable.Set((sourceEndpoint.sourceRef, sourcePrototype, List.empty[String], 0, false)) + val reachable = scala.collection.mutable.Set.empty[String] + val maxDepth = 4 + + while (pending.nonEmpty) { + val (currentRef, currentPrototype, callContext, depth, usedReturnBridge) = pending.dequeue() + val currentPc = pcFromAnyValueRef(currentRef) + val currentIsEntryParameter = isPrototypeEntryParameterRef(currentRef) + val candidateFlows = + (if (depth == 0) argumentFlowsBySourcePrototype else representativeFlowsBySourcePrototype) + .getOrElse(currentPrototype, Vector.empty) + .flatMap(flow => advanceCallContext(flow, callContext).map(flow -> _)) + .filter { flow => + attribution.increment("bridge_argument_provenance_candidate_count") + val pcAccepted = + if (depth == 0) flow._1.callsitePc > source.pc + else if (!flow._1.isArgumentFlow) true + else { + currentPc match { + case Some(pc) if flow._1.callsitePc >= pc => true + case Some(_) => false + case None if currentIsEntryParameter => true + case None => + throw new IllegalStateException( + s"missing pc provenance for cross-module bridge ref: current_ref=$currentRef" + ) + } + } + if (!pcAccepted) { + attribution.increment("bridge_candidate_pc_pruned_count") + attribution.increment("early_candidate_short_circuit_count") + } + pcAccepted + } + + val reachableBridgePrefixes = + reachableRepresentativeBridgePrefixes( + pathSearch, + currentRef, + candidateFlows.map(_._1), + includeArgumentRepresentativeValues = depth > 0 + ) + candidateFlows + .filter { case (flow, _) => reachableBridgePrefixes.contains(flow.fromRef) } + .sortBy { case (flow, _) => (flow.callsitePc, flow.targetPrototype, flow.sortIndex) } + .foreach { case (flow, nextCallContext) => + val nextUsedReturnBridge = usedReturnBridge || !flow.isArgumentFlow + if (nextUsedReturnBridge) reachable += flow.targetPrototype + if (depth < maxDepth) { + val state = (flow.toRef, flow.targetPrototype, nextCallContext, depth + 1, nextUsedReturnBridge) + if (!seen(state)) { + seen += state + pending.enqueue(state) + } + } + } + } + reachable.toSet + } + ) + + private def cachedLocalPath( + pathSearch: LocalPathSearch, + sourceRef: String, + sinkRef: String, + includeRepresentativeCallReturns: Boolean, + includeRepresentativeValueEdges: Boolean + ): Option[Vector[String]] = { + attribution.increment("bridge_local_path_attempt_count") + val key = (sourceRef, sinkRef, includeRepresentativeCallReturns, includeRepresentativeValueEdges) + attribution.increment( + if (localBridgePathCache.contains(key)) "bridge_path_cache_hit_count" else "bridge_path_cache_miss_count" + ) + val result = localBridgePathCache.getOrElseUpdate( + key, + pathSearch.moduleLocalPathMode( + sourceRef, + sinkRef, + includeRepresentativeCallReturns, + includeRepresentativeValueEdges + ) + ) + if (result.nonEmpty) attribution.increment("bridge_local_path_success_count") + result + } + + private def preferredLocalPath( + pathSearch: LocalPathSearch, + sourceRef: String, + sinkRef: String, + includeRepresentativeCallReturns: Boolean + ): Option[Vector[String]] = + preferredLocalPathMode( + pathSearch, + sourceRef, + sinkRef, + includeRepresentativeCallReturns, + includeRepresentativeValueEdges = false + ) + + private def preferredLocalPathForBridgeFlow( + pathSearch: LocalPathSearch, + flow: BridgeFlow, + sinkRef: String + ): Option[Vector[String]] = + if (flow.isArgumentFlow) + preferredLocalPath(pathSearch, flow.toRef, sinkRef, includeRepresentativeCallReturns = true) + else + preferredLocalPathMode( + pathSearch, + flow.toRef, + sinkRef, + includeRepresentativeCallReturns = false, + includeRepresentativeValueEdges = true + ) + + private def preferredLocalPathMode( + pathSearch: LocalPathSearch, + sourceRef: String, + sinkRef: String, + includeRepresentativeCallReturns: Boolean, + includeRepresentativeValueEdges: Boolean + ): Option[Vector[String]] = + cachedLocalPath(pathSearch, sourceRef, sinkRef, includeRepresentativeCallReturns, includeRepresentativeValueEdges) + .map { localPath => + if (crossesSanitizerProducedRef(localPath)) { + pathSearch.moduleLocalPathAvoidingMode( + sourceRef, + sinkRef, + sanitizerProducedRefs, + includeRepresentativeCallReturns, + includeRepresentativeValueEdges + ) match { + case Some(unsanitizedPath) => unsanitizedPath + case None => localPath + } + } else { + localPath + } + } + + private def crossesSanitizerProducedRef(path: Vector[String]): Boolean = + path.exists(sanitizerProducedRefs) + + private def crossesSourceSanitizerProducedRef(path: Vector[String], sourceRef: String): Boolean = { + val sourceModulePath = modulePathFromQualifiedRef(sourceRef) + path.exists(ref => sanitizerProducedRefs(ref) && modulePathFromQualifiedRef(ref) == sourceModulePath) + } + + def pathCrossesSourceSanitizerProducedRef(path: Vector[String], sourceRef: String): Boolean = + crossesSourceSanitizerProducedRef(path, sourceRef) + + private def pcFromAnyValueRef(valueRef: String): Option[Int] = + localRefFromQualifiedRef(valueRef) + .split("@pc", 2) + .lift(1) + .flatMap(_.split(":r", 2).headOption) + .flatMap(_.toIntOption) + + private def requiredCallsitePc(callsiteId: String): Int = + callsitePc(callsiteId).getOrElse(throw new IllegalStateException(s"missing callsite pc: callsite_id=$callsiteId")) + + private def isPrototypeEntryParameterRef(valueRef: String): Boolean = + !localRefFromQualifiedRef(valueRef).contains("@pc") + + private def reachesSinkPrototype(start: String, sinkPrototype: String): Boolean = + reachabilityCache.getOrElseUpdate( + start -> sinkPrototype, { + val pending = scala.collection.mutable.Queue(start) + val seen = scala.collection.mutable.Set.empty[String] + var found = false + while (pending.nonEmpty && !found) { + val current = pending.dequeue() + if (!seen(current)) { + seen += current + outgoingPrototypeEdges.getOrElse(current, Set.empty).foreach { target => + if (target == sinkPrototype) found = true + else if (!seen(target)) pending.enqueue(target) + } + } + } + found + } + ) + + } + + private final class LocalPathSearch(modules: Vector[ModuleSummary], attribution: PerformanceAttributionCollector) { + private final class BlockedRefsIdentityKey(val refs: Set[String]) { + override def equals(other: Any): Boolean = + other match { + case that: BlockedRefsIdentityKey => refs.asInstanceOf[AnyRef] eq that.refs.asInstanceOf[AnyRef] + case _ => false + } + override def hashCode(): Int = System.identityHashCode(refs.asInstanceOf[AnyRef]) + } + + private val modulesByPath = modules.map(module => module.path -> module).toMap + private val graphCache = + scala.collection.mutable.Map.empty[(String, Boolean, Boolean), Map[String, Vector[String]]] + private val pathCache = + scala.collection.mutable.Map.empty[(String, String, Boolean, Boolean), Option[Vector[String]]] + private val avoidingGraphCache = + scala.collection.mutable.Map + .empty[(String, Boolean, Boolean, BlockedRefsIdentityKey), Map[String, Vector[String]]] + private val avoidingPathCache = + scala.collection.mutable.Map + .empty[(String, String, Boolean, Boolean, BlockedRefsIdentityKey), Option[Vector[String]]] + private val representativeSinkPathCache = scala.collection.mutable.Map.empty[String, Vector[Vector[String]]] + private val graphModules = scala.collection.mutable.Set.empty[String] + private var graphBuildCount = 0 + private var localPathSearchCount = 0 + private var batchedDistinctPathCount = 0 + private var uncachedDistinctPathCount = 0 + + def moduleLocalPath( + sourceRef: String, + sinkRef: String, + includeRepresentativeCallReturns: Boolean = false + ): Option[Vector[String]] = + moduleLocalPathMode(sourceRef, sinkRef, includeRepresentativeCallReturns, includeRepresentativeValueEdges = false) + + def moduleLocalPathMode( + sourceRef: String, + sinkRef: String, + includeRepresentativeCallReturns: Boolean, + includeRepresentativeValueEdges: Boolean + ): Option[Vector[String]] = { + attribution.materializeActivePair() + attribution.increment("local_path_search_count") + localPathSearchCount += 1 + val sourceModulePath = modulePathFromQualifiedRef(sourceRef) + val sinkModulePath = modulePathFromQualifiedRef(sinkRef) + if (sourceModulePath != sinkModulePath) { + attribution.increment("local_path_cache_miss_count") + attribution.increment("distinct_local_path_query_count") + uncachedDistinctPathCount += 1 + None + } else { + val key = (sourceRef, sinkRef, includeRepresentativeCallReturns, includeRepresentativeValueEdges) + val hit = pathCache.contains(key) + attribution.increment(if (hit) "local_path_cache_hit_count" else "local_path_cache_miss_count") + if (!hit) attribution.increment("distinct_local_path_query_count") + pathCache.getOrElseUpdate( + key, + modulesByPath.get(sourceModulePath).flatMap { module => + val bySource = graphFor(module, includeRepresentativeCallReturns, includeRepresentativeValueEdges) + breadthFirstPath(bySource, sourceRef, sinkRef) + } + ) + } + } + + def moduleLocalPathAvoiding( + sourceRef: String, + sinkRef: String, + blockedRefs: Set[String], + includeRepresentativeCallReturns: Boolean = false + ): Option[Vector[String]] = + moduleLocalPathAvoidingMode( + sourceRef, + sinkRef, + blockedRefs, + includeRepresentativeCallReturns, + includeRepresentativeValueEdges = false + ) + + def moduleLocalPathAvoidingMode( + sourceRef: String, + sinkRef: String, + blockedRefs: Set[String], + includeRepresentativeCallReturns: Boolean, + includeRepresentativeValueEdges: Boolean + ): Option[Vector[String]] = { + attribution.materializeActivePair() + attribution.increment("local_path_search_count") + localPathSearchCount += 1 + val sourceModulePath = modulePathFromQualifiedRef(sourceRef) + val sinkModulePath = modulePathFromQualifiedRef(sinkRef) + if (sourceModulePath != sinkModulePath || blockedRefs(sourceRef) || blockedRefs(sinkRef)) { + attribution.increment("local_path_cache_miss_count") + attribution.increment("distinct_local_path_query_count") + uncachedDistinctPathCount += 1 + None + } else { + val blockedKey = BlockedRefsIdentityKey(blockedRefs) + val key = (sourceRef, sinkRef, includeRepresentativeCallReturns, includeRepresentativeValueEdges, blockedKey) + val hit = avoidingPathCache.contains(key) + attribution.increment(if (hit) "local_path_cache_hit_count" else "local_path_cache_miss_count") + if (!hit) attribution.increment("distinct_local_path_query_count") + avoidingPathCache.getOrElseUpdate( + key, + modulesByPath.get(sourceModulePath).flatMap { module => + val bySource = avoidingGraphFor( + module, + includeRepresentativeCallReturns, + includeRepresentativeValueEdges, + blockedRefs, + blockedKey + ) + breadthFirstPath(bySource, sourceRef, sinkRef) + } + ) + } + } + + def moduleLocalPathsToAny( + sourceRef: String, + sinkRefs: Set[String], + includeRepresentativeCallReturns: Boolean = false + ): Map[String, Vector[String]] = + moduleLocalPathsToAnyMode( + sourceRef, + sinkRefs, + includeRepresentativeCallReturns, + includeRepresentativeValueEdges = false + ) + + def moduleLocalPathsToAnyWithRepresentativeValues( + sourceRef: String, + sinkRefs: Set[String] + ): Map[String, Vector[String]] = + moduleLocalPathsToAnyMode( + sourceRef, + sinkRefs, + includeRepresentativeCallReturns = true, + includeRepresentativeValueEdges = true + ) + + private def moduleLocalPathsToAnyMode( + sourceRef: String, + sinkRefs: Set[String], + includeRepresentativeCallReturns: Boolean, + includeRepresentativeValueEdges: Boolean + ): Map[String, Vector[String]] = + if (sinkRefs.isEmpty) Map.empty + else { + attribution.materializeActivePair() + attribution.increment("local_path_search_count") + attribution.increment("local_path_cache_miss_count") + attribution.increment("distinct_local_path_query_count") + localPathSearchCount += 1 + batchedDistinctPathCount += 1 + val sourceModulePath = modulePathFromQualifiedRef(sourceRef) + val invalidSinkModules = sinkRefs + .map(modulePathFromQualifiedRef) + .filter(_ != sourceModulePath) + if (invalidSinkModules.nonEmpty) { + throw new IllegalStateException( + s"cross-module refs in local path batch: source_ref=$sourceRef sink_modules=${invalidSinkModules.toVector.sorted + .mkString("[", ",", "]")}" + ) + } + modulesByPath + .get(sourceModulePath) + .map { module => + val bySource = graphFor(module, includeRepresentativeCallReturns, includeRepresentativeValueEdges) + breadthFirstPathsToAny(bySource, sourceRef, sinkRefs) + } + .getOrElse(Map.empty) + } + + def representativeSinkPaths( + module: ModuleSummary, + sinkEndpoint: LuaSinkEndpoint, + seeds: => Vector[String] + ): Vector[Vector[String]] = + representativeSinkPathCache.getOrElseUpdate( + sinkEndpoint.sinkRef, + seeds.flatMap(seed => moduleLocalPath(seed, sinkEndpoint.sinkRef)) + ) + + def stats(sourceSinkPairCount: Int, qualifiedSourceSinkPairCount: Int): LuaPathSearchStats = { + val distinctLocalPathQueryCount = + pathCache.size + avoidingPathCache.size + batchedDistinctPathCount + uncachedDistinctPathCount + LuaPathSearchStats( + localPathGraphModuleCount = graphModules.size, + localPathGraphBuildCount = graphBuildCount, + localPathSearchCount = localPathSearchCount, + distinctLocalPathQueryCount = distinctLocalPathQueryCount, + sourceSinkPairCount = sourceSinkPairCount, + qualifiedSourceSinkPairCount = qualifiedSourceSinkPairCount, + prototypePrunedSourceSinkPairCount = sourceSinkPairCount - qualifiedSourceSinkPairCount + ) + } + + private def graphFor( + module: ModuleSummary, + includeRepresentativeCallReturns: Boolean, + includeRepresentativeValueEdges: Boolean + ): Map[String, Vector[String]] = { + val key = (module.path, includeRepresentativeCallReturns, includeRepresentativeValueEdges) + val hit = graphCache.contains(key) + attribution.increment(if (hit) "local_path_graph_cache_hit_count" else "local_path_graph_cache_miss_count") + if (!hit) attribution.increment("local_path_graph_build_count") + graphCache.getOrElseUpdate( + key, { + graphBuildCount += 1 + graphModules += module.path + val localEdges = module.localFlows + .map(flow => qualify(module.path, flow.sourceRef) -> qualify(module.path, flow.sinkRef)) + val globalEdges = + module.globalFlows.map(flow => qualify(module.path, flow.writeRef) -> qualify(module.path, flow.readRef)) + val representativeCallReturn = + if (includeRepresentativeCallReturns) + representativeCallReturnEdges(module) + else Vector.empty + val representativeValueEdges = + if (includeRepresentativeValueEdges) + representativeTableValueEdges(module) ++ + representativeParameterTableReadEdges(module) ++ + representativeIteratorValueEdges(module) ++ + representativeExpressionResultEdges(module) + else Vector.empty + (localEdges ++ globalEdges ++ representativeCallReturn ++ representativeValueEdges).groupMap(_._1)(_._2) + } + ) + } + + private def avoidingGraphFor( + module: ModuleSummary, + includeRepresentativeCallReturns: Boolean, + includeRepresentativeValueEdges: Boolean, + blockedRefs: Set[String], + blockedKey: BlockedRefsIdentityKey + ): Map[String, Vector[String]] = + avoidingGraphCache.getOrElseUpdate( + (module.path, includeRepresentativeCallReturns, includeRepresentativeValueEdges, blockedKey), { + graphFor(module, includeRepresentativeCallReturns, includeRepresentativeValueEdges).flatMap { + case (source, targets) => + if (blockedRefs(source)) None + else { + val retainedTargets = targets.filterNot(blockedRefs) + Option.when(retainedTargets.nonEmpty)(source -> retainedTargets) + } + } + } + ) + + private def breadthFirstPath( + bySource: Map[String, Vector[String]], + sourceRef: String, + sinkRef: String + ): Option[Vector[String]] = { + val queue = scala.collection.mutable.Queue(Vector(sourceRef)) + val seen = scala.collection.mutable.Set(sourceRef) + var found: Option[Vector[String]] = None + while (queue.nonEmpty && found.isEmpty) { + val path = queue.dequeue() + attribution.increment("targeted_search_node_visit_count") + if (path.last == sinkRef) found = Some(path) + else { + bySource.getOrElse(path.last, Vector.empty).foreach { next => + attribution.increment("targeted_search_edge_visit_count") + if (!seen(next)) { + seen += next + queue.enqueue(path :+ next) + } + } + } + } + found + } + + private def breadthFirstPathsToAny( + bySource: Map[String, Vector[String]], + sourceRef: String, + sinkRefs: Set[String] + ): Map[String, Vector[String]] = { + val remaining = scala.collection.mutable.Set.from(sinkRefs) + val found = scala.collection.mutable.Map.empty[String, Vector[String]] + val queue = scala.collection.mutable.Queue(Vector(sourceRef)) + val seen = scala.collection.mutable.Set(sourceRef) + while (queue.nonEmpty && remaining.nonEmpty) { + val path = queue.dequeue() + attribution.increment("targeted_search_node_visit_count") + if (remaining(path.last)) { + found += path.last -> path + remaining -= path.last + } + if (remaining.nonEmpty) { + bySource.getOrElse(path.last, Vector.empty).foreach { next => + attribution.increment("targeted_search_edge_visit_count") + if (!seen(next)) { + seen += next + queue.enqueue(path :+ next) + } + } + } + } + found.toMap + } + } + + private final class SourceSinkPruning( + modules: Vector[ModuleSummary], + fieldTargets: Vector[LuaModuleFieldCallTarget], + interproceduralArgFlows: Vector[LuaInterproceduralArgFlow], + interproceduralReturnFlows: Vector[LuaInterproceduralReturnFlow], + sinkEndpoints: Vector[LuaSinkEndpoint], + attribution: PerformanceAttributionCollector + ) { + private val callGraphAdjacency = buildCallGraphAdjacency() + private val sinkPrototypeByRef = sinkEndpoints.map(row => row.sinkRef -> prototypeRef(row.sinkRef)).toMap + private val directReachableSinkRefsBySource = buildDirectReachableSinkRefsBySource() + private val directInterproceduralPairs = interproceduralArgFlows.map { flow => + val source = parseQualifiedValueRef(flow.fromArgumentRef) + prototypeRef(source.modulePath, source.prototypeId) -> prototypeRef(flow.targetModulePath, flow.targetPrototypeId) + }.toSet + private val fedArgumentIndexesBySourceAndTarget = interproceduralArgFlows + .groupMap { flow => + val source = parseQualifiedValueRef(flow.fromArgumentRef) + prototypeRef(source.modulePath, source.prototypeId) -> prototypeRef( + flow.targetModulePath, + flow.targetPrototypeId + ) + }(_.argumentIndex) + .view + .mapValues(_.toSet) + .toMap + private val interproceduralFlowsBySourcePrototype = interproceduralArgFlows.groupBy { flow => + val source = parseQualifiedValueRef(flow.fromArgumentRef) + prototypeRef(source.modulePath, source.prototypeId) + } + private val reachablePrototypesByStartPrototype = scala.collection.mutable.Map.empty[String, Set[String]] + private val sourceScopedBridgePrototypeCache = scala.collection.mutable.Map.empty[String, Set[String]] + + def qualifiedSinkEndpoints( + sourceEndpoint: LuaSourceEndpoint, + allSinkEndpoints: Vector[LuaSinkEndpoint] + ): Vector[LuaSinkEndpoint] = { + val sourcePrototypeRef = prototypeRef(sourceEndpoint.sourceRef) + val reachablePrototypes = reachableFrom(callGraphAdjacency, sourcePrototypeRef) + val sourceSpecificSinks = directReachableSinkRefsBySource.getOrElse(sourcePrototypeRef, Set.empty) + val missingProvenanceRefs = Vector.newBuilder[String] + val qualified = allSinkEndpoints.filter { sinkEndpoint => + val sinkPrototypeRef = sinkPrototypeByRef + .get(sinkEndpoint.sinkRef) + .getOrElse( + throw new IllegalStateException( + s"missing sink prototype mapping for path-search prefilter: source_ref=${sourceEndpoint.sourceRef} sink_ref=${sinkEndpoint.sinkRef}" + ) + ) + attribution.incrementAggregate("source_reachability_check_count") + val reachesAndHasSourceSpecificProvenance = + if (!reachablePrototypes(sinkPrototypeRef)) { + attribution.incrementAggregate("prototype_unreachable_pair_count") + false + } else if (sinkPrototypeRef == sourcePrototypeRef) { + attribution.incrementAggregate("source_reachability_accepted_count") + true + } else if (sourceSpecificSinks.isEmpty) { + missingProvenanceRefs += sinkEndpoint.sinkRef + attribution.incrementAggregate("source_specific_provenance_pruned_pair_count") + false + } else { + val accepted = sourceSpecificSinks(sinkEndpoint.sinkRef) + attribution.incrementAggregate( + if (accepted) "source_reachability_accepted_count" + else "source_specific_provenance_pruned_pair_count" + ) + accepted + } + val accepted = if (!reachesAndHasSourceSpecificProvenance) { + false + } else { + attribution.incrementAggregate("path_constructor_check_count") + val constructorAccepted = pathConstructorCanAttempt(sourceEndpoint, sinkEndpoint) + attribution.incrementAggregate( + if (constructorAccepted) "path_constructor_accepted_count" else "path_constructor_pruned_count" + ) + if (!constructorAccepted) false + else { + attribution.incrementAggregate("parameter_position_check_count") + val parameterAccepted = parameterPositionAllows(sourcePrototypeRef, sinkPrototypeRef, sinkEndpoint) + attribution.incrementAggregate( + if (parameterAccepted) "parameter_position_accepted_count" else "parameter_position_pruned_count" + ) + parameterAccepted + } + } + accepted + } + val missing = missingProvenanceRefs.result() + if (missing.nonEmpty) { + throw new IllegalStateException( + "missing source-specific sink reachability provenance for path-search prefilter: " + + s"source_ref=${sourceEndpoint.sourceRef} source_prototype_ref=$sourcePrototypeRef missing_sink_refs=${missing.sorted + .mkString("[", ",", "]")}" + ) + } + qualified + } + + private def parameterPositionAllows( + sourcePrototypeRef: String, + sinkPrototypeRef: String, + sinkEndpoint: LuaSinkEndpoint + ): Boolean = + if (sinkPrototypeRef == sourcePrototypeRef) true + else { + val mappingKey = sourcePrototypeRef -> sinkPrototypeRef + if (!directInterproceduralPairs(mappingKey)) true + else { + fedArgumentIndexesBySourceAndTarget.get(mappingKey) match { + case Some(fedIndexes) => fedIndexes(sinkEndpoint.parameterIndex) + case None => + throw new IllegalStateException( + "missing fed-argument provenance for parameter-position filter: " + + s"source_prototype_ref=$sourcePrototypeRef sink_ref=${sinkEndpoint.sinkRef}" + ) + } + } + } + + private def pathConstructorCanAttempt(sourceEndpoint: LuaSourceEndpoint, sinkEndpoint: LuaSinkEndpoint): Boolean = { + val source = parseQualifiedValueRef(sourceEndpoint.sourceRef) + val sink = parseQualifiedValueRef(sinkEndpoint.sinkRef) + val bridgePrototypes = sourceScopedBridgePrototypes(sourceEndpoint.sourceRef, source) + val samePrototypeForward = + source.modulePath == sink.modulePath && source.prototypeId == sink.prototypeId && source.pc <= sink.pc + samePrototypeForward || bridgePrototypes(prototypeRef(sink.modulePath, sink.prototypeId)) + } + + private def sourceScopedBridgePrototypes(sourceRef: String, source: QualifiedValueRef): Set[String] = + sourceScopedBridgePrototypeCache.getOrElseUpdate( + sourceRef, { + val sourcePrototype = prototypeRef(source.modulePath, source.prototypeId) + interproceduralFlowsBySourcePrototype + .getOrElse(sourcePrototype, Vector.empty) + .filter { flow => + attribution.incrementAggregate("bridge_argument_provenance_candidate_count") + val accepted = requiredCallsitePc(flow.callsiteId) >= source.pc + if (!accepted) { + attribution.incrementAggregate("bridge_candidate_pc_pruned_count") + attribution.incrementAggregate("early_candidate_short_circuit_count") + } + accepted + } + .flatMap { flow => + val targetPrototype = prototypeRef(flow.targetModulePath, flow.targetPrototypeId) + reachablePrototypesIncludingSelf(targetPrototype) + } + .toSet + } + ) + + private def reachablePrototypesIncludingSelf(startPrototype: String): Set[String] = + reachablePrototypesByStartPrototype.getOrElseUpdate( + startPrototype, + reachableFrom(callGraphAdjacency, startPrototype) + ) + + private def buildCallGraphAdjacency(): Map[String, Set[String]] = { + val adjacency = scala.collection.mutable.Map.empty[String, scala.collection.mutable.Set[String]] + def addNode(ref: String): Unit = + adjacency.getOrElseUpdate(ref, scala.collection.mutable.Set.empty) + def addEdge(source: String, target: String): Unit = { + addNode(source) + addNode(target) + adjacency(source) += target + } + + modules.foreach { module => + module.prototypes.foreach(prototype => addNode(prototypeRef(module.path, prototype.prototypeId))) + module.localCalls.foreach { call => + addEdge( + prototypeRef(module.path, prototypeIdFromCallsiteId(call.callsiteId)), + prototypeRef(module.path, call.targetPrototypeId) + ) + } + } + fieldTargets.foreach { target => + addEdge( + prototypeRef(target.fromModulePath, prototypeIdFromCallsiteId(target.callsiteId)), + prototypeRef(target.targetModulePath, target.targetPrototypeId) + ) + } + interproceduralArgFlows.foreach { flow => + val source = parseQualifiedValueRef(flow.fromArgumentRef) + addEdge( + prototypeRef(source.modulePath, source.prototypeId), + prototypeRef(flow.targetModulePath, flow.targetPrototypeId) + ) + } + interproceduralReturnFlows.foreach { flow => + val caller = parseQualifiedValueRef(flow.callerResultRef) + addEdge( + prototypeRef(flow.targetModulePath, flow.targetPrototypeId), + prototypeRef(caller.modulePath, caller.prototypeId) + ) + } + adjacency.view.mapValues(_.toSet).toMap + } + + private def buildDirectReachableSinkRefsBySource(): Map[String, Set[String]] = { + val sinkRefsByPrototype = sinkPrototypeByRef.groupMap(_._2)(_._1).view.mapValues(_.toSet).toMap + val reverseAdjacency = scala.collection.mutable.Map.empty[String, scala.collection.mutable.Set[String]] + callGraphAdjacency.foreach { case (sourcePrototype, targetPrototypes) => + reverseAdjacency.getOrElseUpdate(sourcePrototype, scala.collection.mutable.Set.empty) + targetPrototypes.foreach { targetPrototype => + reverseAdjacency.getOrElseUpdate(targetPrototype, scala.collection.mutable.Set.empty) += sourcePrototype + } + } + + val result = scala.collection.mutable.Map.empty[String, scala.collection.mutable.Set[String]] + sinkRefsByPrototype.foreach { case (sinkPrototypeRef, sinkRefs) => + val pending = scala.collection.mutable.Queue(sinkPrototypeRef) + val seen = scala.collection.mutable.Set.empty[String] + while (pending.nonEmpty) { + val prototypeRef = pending.dequeue() + if (!seen(prototypeRef)) { + seen += prototypeRef + result.getOrElseUpdate(prototypeRef, scala.collection.mutable.Set.empty) ++= sinkRefs + reverseAdjacency.getOrElse(prototypeRef, Set.empty).foreach(pending.enqueue(_)) + } + } + } + result.view.mapValues(_.toSet).toMap + } + } + + private def reachableFrom(graph: Map[String, Set[String]], source: String): Set[String] = { + val pending = scala.collection.mutable.Queue(source) + val seen = scala.collection.mutable.Set.empty[String] + while (pending.nonEmpty) { + val current = pending.dequeue() + if (!seen(current)) { + seen += current + graph.getOrElse(current, Set.empty).diff(seen.toSet).toVector.sorted.foreach(pending.enqueue(_)) + } + } + seen.toSet + } + + private def prototypeRef(valueRef: String): String = { + val parsed = parseQualifiedValueRef(valueRef) + prototypeRef(parsed.modulePath, parsed.prototypeId) + } + + private def prototypeRefFromAnyQualifiedRef(valueRef: String): String = + prototypeRef(modulePathFromQualifiedRef(valueRef), prototypeIdFromLocalValueRef(localRefFromQualifiedRef(valueRef))) + + private def prototypeRef(modulePath: String, prototypeId: String): String = s"$modulePath::$prototypeId" + + private def prototypeIdFromLocalValueRef(valueRef: String): String = + valueRef.split("@pc", 2).headOption.getOrElse(valueRef).split(":r", 2).headOption.getOrElse(valueRef) + + private def representativeCallReturnEdges(module: ModuleSummary): Vector[(String, String)] = + (module.prototypes.flatMap { prototype => + prototype.calls + .filter(call => isRepresentativeTaintPreservingCall(call.resolvedName)) + .flatMap { call => + call.returnRefs.headOption.toVector.flatMap { returnRef => + call.argumentRefs.map(argumentRef => qualify(module.path, argumentRef) -> qualify(module.path, returnRef)) + } + } + } ++ module.fieldCalls + .filter(call => isRepresentativeFieldReturnCall(call)) + .flatMap { call => + call.resultRef.toVector.flatMap { returnRef => + call.argumentRefs.map(argumentRef => qualify(module.path, argumentRef) -> qualify(module.path, returnRef)) + } + }).distinct + + private def isRepresentativeFieldReturnCall(call: FieldCall): Boolean = + call.resolvedName.exists(name => isRepresentativeTaintPreservingCall(Some(name))) + + private def isRepresentativeTaintPreservingCall(name: Option[String]): Boolean = + name.exists(resolved => + isRepresentativeSanitizerCall(Some(resolved)) || + Set("ciphertextFormat", "json.encode", "cjson.encode", "string.format", "string.lower", "string.upper") + .contains(resolved) + ) + + private def isRepresentativeIteratorCall(name: Option[String]): Boolean = + name.exists(Set("ipairs", "pairs").contains) + + private def representativeTableValueEdges(module: ModuleSummary): Vector[(String, String)] = + module.prototypes.flatMap { prototype => + val tableSourcesBySlot = scala.collection.mutable.Map.empty[Int, Set[String]].withDefaultValue(Set.empty) + val fieldSourcesBySlotAndKey = + scala.collection.mutable.Map.empty[(Int, LuaConstantValue), Set[String]].withDefaultValue(Set.empty) + val edges = Vector.newBuilder[(String, String)] + def fixedKey(encoded: Int): Option[LuaConstantValue] = + Option.when(encoded >= RkConstantBase)(encoded - RkConstantBase).flatMap { index => + prototype.constants.collectFirst { case LuaConstant(`index`, _, value) => value } + } + prototype.instructions.sortBy(_.pc).foreach { instruction => + if (instruction.opcode == LuaOpcode.GetTable && tableSourcesBySlot(instruction.b).nonEmpty) { + val tableRead = valueRef(prototype.prototypeId, instruction.pc, instruction.b) + val result = valueRef(prototype.prototypeId, instruction.pc, instruction.a) + edges += qualify(module.path, tableRead) -> qualify(module.path, result) + } + if (instruction.opcode == LuaOpcode.GetTable) { + instruction.c.flatMap(fixedKey).foreach { fieldKey => + val sources = fieldSourcesBySlotAndKey((instruction.b, fieldKey)) + if (sources.nonEmpty) { + val tableRead = valueRef(prototype.prototypeId, instruction.pc, instruction.b) + val result = valueRef(prototype.prototypeId, instruction.pc, instruction.a) + sources.foreach { source => + edges += qualify(module.path, source) -> qualify(module.path, tableRead) + } + edges += qualify(module.path, tableRead) -> qualify(module.path, result) + } + } + } + representativeReadSlots(prototype, instruction).foreach { slot => + val read = valueRef(prototype.prototypeId, instruction.pc, slot) + tableSourcesBySlot(slot).foreach { source => + edges += qualify(module.path, source) -> qualify(module.path, read) + } + } + if (instruction.opcode == LuaOpcode.SetTable) { + for { + valueSlot <- instruction.c.filter(_ < RkConstantBase) + fieldKey <- fixedKey(instruction.b) + } { + val valueRead = valueRef(prototype.prototypeId, instruction.pc, valueSlot) + fieldSourcesBySlotAndKey += ((instruction.a, fieldKey) -> + (fieldSourcesBySlotAndKey((instruction.a, fieldKey)) + valueRead)) + } + } + if (instruction.opcode == LuaOpcode.SetList && instruction.b > 0) { + val tableRead = valueRef(prototype.prototypeId, instruction.pc, instruction.a) + setListValueSlots(instruction).foreach { valueSlot => + val valueRead = valueRef(prototype.prototypeId, instruction.pc, valueSlot) + edges += qualify(module.path, valueRead) -> qualify(module.path, tableRead) + tableSourcesBySlot += instruction.a -> (tableSourcesBySlot(instruction.a) + valueRead) + } + } + } + edges.result() + } + + private def representativeParameterTableReadEdges(module: ModuleSummary): Vector[(String, String)] = + module.prototypes.flatMap { prototype => + val parameterSlots = (0 until prototype.numParams).toSet + prototype.instructions.collect { + case instruction + if instruction.opcode == LuaOpcode.GetTable && + parameterSlots(instruction.b) && + instruction.c.exists(_ >= RkConstantBase) => + qualify(module.path, valueRef(prototype.prototypeId, instruction.pc, instruction.b)) -> + qualify(module.path, valueRef(prototype.prototypeId, instruction.pc, instruction.a)) + } + } + + private def representativeIteratorValueEdges(module: ModuleSummary): Vector[(String, String)] = + module.prototypes.flatMap { prototype => + prototype.calls + .filter(call => isRepresentativeIteratorCall(call.resolvedName)) + .flatMap { iteratorCall => + val iteratorInputs = iteratorCall.argumentRefs.map(qualify(module.path, _)) + for { + callInstruction <- prototype.instructions + .find(instruction => instruction.pc == iteratorCall.pc && instruction.opcode == LuaOpcode.Call) + .toVector + loop <- ipairsTForLoop(prototype, callInstruction).toVector + input <- iteratorInputs + edge <- tforLoopTableReadEdges(module.path, prototype, loop, input) + } yield edge + } + }.distinct + + private final case class TForLoopRegion(bodyStartPc: Int, loopInstruction: LuaInstruction) + + private def ipairsTForLoop(prototype: PrototypeSummary, iteratorCall: LuaInstruction): Option[TForLoopRegion] = + for { + jump <- prototype.instructions.find(instruction => + instruction.pc == iteratorCall.pc + 1 && instruction.opcode == LuaOpcode.Jmp + ) + tforLoop <- prototype.instructions + .filter(instruction => + instruction.opcode == LuaOpcode.TForLoop && + instruction.a == iteratorCall.a && + instruction.pc > jump.pc + ) + .sortBy(_.pc) + .headOption + } yield TForLoopRegion(jump.pc, tforLoop) + + private def tforLoopTableReadEdges( + modulePath: String, + prototype: PrototypeSummary, + loop: TForLoopRegion, + iteratorInput: String + ): Vector[(String, String)] = { + val tforLoop = loop.loopInstruction + val loopValueSlots = tforLoop.c + .map(count => (tforLoop.a + 3 until tforLoop.a + 3 + count).toSet) + .getOrElse(Set.empty) + val bodyInstructions = prototype.instructions + .filter(instruction => instruction.pc > loop.bodyStartPc && instruction.pc < tforLoop.pc) + val loopValueReadEdges = bodyInstructions.flatMap { instruction => + representativeReadSlots(prototype, instruction) + .filter(loopValueSlots) + .map(slot => iteratorInput -> qualify(modulePath, valueRef(prototype.prototypeId, instruction.pc, slot))) + } + val tableReadEdges = bodyInstructions.collect { + case instruction + if instruction.opcode == LuaOpcode.GetTable && + loopValueSlots(instruction.b) && + instruction.c.exists(_ >= RkConstantBase) => + val tableRead = qualify(modulePath, valueRef(prototype.prototypeId, instruction.pc, instruction.b)) + val fieldRead = qualify(modulePath, valueRef(prototype.prototypeId, instruction.pc, instruction.a)) + Vector(iteratorInput -> tableRead, tableRead -> fieldRead) + }.flatten + (loopValueReadEdges ++ tableReadEdges).distinct + } + + private def representativeExpressionResultEdges(module: ModuleSummary): Vector[(String, String)] = + module.prototypes.flatMap { prototype => + val sortedInstructions = prototype.instructions.sortBy(_.pc) + sortedInstructions.flatMap { instruction => + val result = valueRef(prototype.prototypeId, instruction.pc, instruction.a) + val operandEdges = instruction.opcode match { + case LuaOpcode.Add | LuaOpcode.Sub | LuaOpcode.Mul | LuaOpcode.Div | LuaOpcode.Mod | LuaOpcode.Pow => + (Vector(Some(instruction.b)) ++ Vector(instruction.c)).flatten + .filter(_ < RkConstantBase) + .map(slot => + qualify(module.path, valueRef(prototype.prototypeId, instruction.pc, slot)) -> qualify( + module.path, + result + ) + ) + case LuaOpcode.Concat => + (instruction.b to instruction.c.getOrElse(instruction.b)).toVector + .map(slot => + qualify(module.path, valueRef(prototype.prototypeId, instruction.pc, slot)) -> qualify( + module.path, + result + ) + ) + case _ => Vector.empty + } + val reachingEdges = + if (operandEdges.nonEmpty) + expressionResultReadEdges(module.path, prototype, sortedInstructions, instruction) + else Vector.empty + operandEdges ++ reachingEdges + } + }.distinct + + private def expressionResultReadEdges( + modulePath: String, + prototype: PrototypeSummary, + sortedInstructions: Vector[LuaInstruction], + expression: LuaInstruction + ): Vector[(String, String)] = { + val resultSlot = expression.a + val resultRef = valueRef(prototype.prototypeId, expression.pc, resultSlot) + val edges = Vector.newBuilder[(String, String)] + var stopped = false + sortedInstructions + .filter(_.pc > expression.pc) + .foreach { instruction => + if (!stopped) { + representativeReadSlots(prototype, instruction) + .filter(_ == resultSlot) + .foreach(slot => + edges += qualify(modulePath, resultRef) -> qualify( + modulePath, + valueRef(prototype.prototypeId, instruction.pc, slot) + ) + ) + if (overwritesSlot(instruction, resultSlot)) { + stopped = true + } + } + } + edges.result() + } + + private def overwritesSlot(instruction: LuaInstruction, slot: Int): Boolean = + instruction.opcode match { + case LuaOpcode.SetGlobal | LuaOpcode.SetUpval | LuaOpcode.SetTable | LuaOpcode.SetList | LuaOpcode.Return | + LuaOpcode.Eq | LuaOpcode.Lt | LuaOpcode.Le | LuaOpcode.Test | LuaOpcode.Jmp => + false + case LuaOpcode.Call | LuaOpcode.TailCall => + instruction.c match { + case Some(0) => instruction.a <= slot + case Some(1) => false + case Some(n) => slot >= instruction.a && slot < instruction.a + n - 1 + case None => false + } + case _ => instruction.a == slot + } + + private def representativeReadSlots(prototype: PrototypeSummary, instruction: LuaInstruction): Vector[Int] = + instruction.opcode match { + case LuaOpcode.Move => Vector(instruction.b) + case LuaOpcode.GetTable => Vector(Some(instruction.b), instruction.c.filter(_ < RkConstantBase)).flatten + case LuaOpcode.SetTable => + Vector( + Some(instruction.a), + Some(instruction.b).filter(_ < RkConstantBase), + instruction.c.filter(_ < RkConstantBase) + ).flatten + case LuaOpcode.SetList => Vector(instruction.a) ++ setListValueSlots(instruction) + case LuaOpcode.Call | LuaOpcode.TailCall => + (Vector(instruction.a) ++ callArgumentRefs(prototype, instruction).map(slotFromLocalValueRef)).distinct + case LuaOpcode.Return => + instruction.b match { + case 0 | 1 => Vector.empty + case n => (instruction.a until (instruction.a + n - 1)).toVector + } + case LuaOpcode.Eq | LuaOpcode.Lt | LuaOpcode.Le => + Vector(Some(instruction.b).filter(_ < RkConstantBase), instruction.c.filter(_ < RkConstantBase)).flatten + case LuaOpcode.Add | LuaOpcode.Sub | LuaOpcode.Mul | LuaOpcode.Div | LuaOpcode.Mod | LuaOpcode.Pow => + Vector(Some(instruction.b).filter(_ < RkConstantBase), instruction.c.filter(_ < RkConstantBase)).flatten + case LuaOpcode.Unm | LuaOpcode.Not | LuaOpcode.Len => + Vector(instruction.b) + case LuaOpcode.Concat => + (instruction.b to instruction.c.getOrElse(instruction.b)).toVector + case _ => Vector.empty + } + + private def setListValueSlots(instruction: LuaInstruction): Vector[Int] = + if (instruction.b > 0) (1 to instruction.b).map(offset => instruction.a + offset).toVector else Vector.empty + + private def valueRef(prototypeId: String, pc: Int, slot: Int): String = + s"$prototypeId@pc$pc:r$slot" + + private def slotFromLocalValueRef(ref: String): Int = + ref + .split(":r", 2) + .lift(1) + .flatMap(_.toIntOption) + .getOrElse(throw new IllegalArgumentException(s"Lua local value ref is missing slot: $ref")) + + private def sourceEndpointsForModule(module: ModuleSummary): Vector[LuaSourceEndpoint] = { + val directEndpoints = module.prototypes.flatMap { prototype => + prototype.calls.flatMap { call => + call.resolvedName + .filter(name => triggerMatches("*.formvalue", name)) + .flatMap(name => + call.returnRefs.headOption.map(sourceRef => + LuaSourceEndpoint( + qualify(module.path, sourceRef), + qualify(module.path, call.callsiteId), + name, + Provenance + ) + ) + ) + } + } + val requireStringsByRef = module.requireCalls.flatMap(call => call.resultRef.zip(call.requireString)).toMap + val requireFieldEndpoints = module.fieldCalls.flatMap { call => + for { + requireRef <- call.requireRef + requireString <- requireStringsByRef.get(requireRef) + if requireString == "luci.http" && call.fieldName == "formvalue" + resultRef <- call.resultRef + } yield LuaSourceEndpoint( + qualify(module.path, resultRef), + qualify(module.path, call.callsiteId), + s"$requireString.${call.fieldName}", + Provenance + ) + } + preferCanonicalSourceEndpoints(directEndpoints ++ requireFieldEndpoints) + } + + private def preferCanonicalSourceEndpoints(endpoints: Vector[LuaSourceEndpoint]): Vector[LuaSourceEndpoint] = + endpoints + .groupBy(_.sourceRef) + .values + .map { candidates => + candidates.sortBy(endpoint => if (endpoint.trigger == "luci.http.formvalue") 0 else 1).head + } + .toVector + + private def sinkEndpointsForModule( + module: ModuleSummary, + attribution: PerformanceAttributionCollector + ): Vector[LuaSinkEndpoint] = + (module.prototypes.flatMap { prototype => + prototype.calls.flatMap { call => + call.resolvedName + .flatMap(name => directCommandSinkTrigger(module, name)) + .flatMap { name => + sinkValueRef(call) + .filter { argumentRef => + val accepted = !isConcreteStringArgument(prototype, call.pc, argumentRef) + attribution.recordP1(accepted) + accepted + } + .map(argumentRef => + LuaSinkEndpoint( + qualify(module.path, argumentRef), + qualify(module.path, call.callsiteId), + canonicalSinkTrigger(name), + 0, + Provenance + ) + ) + } + } + } ++ module.fieldCalls + .flatMap(call => + fieldCallSinkTrigger(module, call).flatMap { trigger => + for { + prototype <- module.prototype(prototypeIdFromCallsiteId(call.callsiteId)) + pc <- callsitePc(call.callsiteId) + argumentRef <- call.argumentRefs.headOption + accepted = !isConcreteStringArgument(prototype, pc, argumentRef) + _ = attribution.recordP1(accepted) + if accepted + } yield LuaSinkEndpoint( + qualify(module.path, argumentRef), + qualify(module.path, call.callsiteId), + trigger, + 0, + Provenance + ) + } + )).distinct + + private def directCommandSinkTrigger(module: ModuleSummary, name: String): Option[String] = + name match { + case "os.execute" | "io.popen" => Some(canonicalSinkTrigger(name)) + case "forkExec" if modulePathDeclaresRequire(module.path, "xiaoqiang.common.XQFunction") => + Some("forkExec") + case _ => None + } + + private def fieldCallSinkTrigger(module: ModuleSummary, call: FieldCall): Option[String] = + if ( + call.fieldName == "exec" && + (call.resolvedName.contains("luci.util.exec") || + fieldCallRequirePath(module, call).contains(modulePathForRequire("luci.util"))) + ) { + Some("luci.util.exec") + } else if ( + call.fieldName == "forkExec" && + fieldCallRequirePath(module, call).contains(modulePathForRequire("xiaoqiang.common.XQFunction")) + ) { + Some("xiaoqiang.common.XQFunction.forkExec") + } else None + + private def fieldCallRequirePath(module: ModuleSummary, call: FieldCall): Option[String] = + ( + call.requireRef.flatMap(module.requireResultRefs.get).toVector ++ + capturedUpvalueRequirePath(module, call).toVector + ).distinct match { + case Vector(single) => Some(single) + case Vector() => None + case many => + throw new IllegalStateException( + s"ambiguous field-call require path: module=${module.path} callsite=${call.callsiteId} paths=${many.sorted + .mkString("[", ",", "]")}" + ) + } + + private def capturedUpvalueRequirePath(module: ModuleSummary, call: FieldCall): Option[String] = + for { + targetRef <- call.targetRef + target <- registerWrite(targetRef) + prototype <- module.prototype(prototypeIdFromCallsiteId(call.callsiteId)) + getTable <- prototype.instructions.find(instruction => + instruction.pc == target.pc && + instruction.a == target.slot && + instruction.opcode == LuaOpcode.GetTable + ) + upvalueSlot <- getUpvalueBefore(prototype, getTable.pc, getTable.b) + requireRef <- module.capturedRequireRefs.get(capturedRequireRefKey(prototype.prototypeId, upvalueSlot)) + requirePath <- module.requireResultRefs.get(requireRef) + } yield requirePath + + private def canonicalSinkTrigger(name: String): String = + name match { + case "require.popen" => "io.popen" + case other => other + } + + private def sinkValueRef(call: ResolvedCall): Option[String] = + call.argumentRefs.headOption + + private def isRepresentativeSanitizerCall(name: Option[String]): Boolean = + name.exists { resolved => + resolved == "tonumber" || + resolved == "tostring" || + resolved == "string.format" || + resolved.endsWith("._cmdformat") || + resolved.endsWith(".macFormat") || + resolved.endsWith(".binaryBase64Enc") + } + + private def ruleMatchesFor( + sourceEndpoints: Vector[LuaSourceEndpoint], + sinkEndpoints: Vector[LuaSinkEndpoint] + ): Vector[LuaRuleMatch] = { + val sourceRules = + sourceEndpoints.map(row => LuaRuleMatch(row.callsiteId, "source", "formvalue", row.trigger, None, row.provenance)) + val sinkRules = sinkEndpoints.map(row => + LuaRuleMatch( + row.callsiteId, + "sink", + finalSegment(row.trigger), + row.trigger, + Some(row.parameterIndex), + row.provenance + ) + ) + sourceRules ++ sinkRules + } + + private def sanitizerCallsFor(modules: Vector[ModuleSummary]): Vector[LuaSanitizerCall] = + modules.flatMap(sanitizerCallsForModule).distinct + + private def sanitizerCallsForModule(module: ModuleSummary): Vector[LuaSanitizerCall] = { + val resolvedCalls = module.prototypes.flatMap { prototype => + prototype.calls.flatMap { call => + for { + name <- call.resolvedName + if isRealFirmwareSanitizerName(name) + sanitizedValueRef <- sanitizerProducedValueRef(prototype, call) + } yield LuaSanitizerCall( + qualify(module.path, call.callsiteId), + name, + qualify(module.path, sanitizedValueRef), + Provenance + ) + } + } + resolvedCalls.distinct + } + + private def realFirmwareSanitizerProducedRefs(modules: Vector[ModuleSummary]): Set[String] = + modules + .flatMap(sanitizerCallsForModule) + .map(_.sanitizedValueRef) + .toSet + + private def isRealFirmwareSanitizerName(name: String): Boolean = + RealFirmwareSanitizerSuffixes.contains(name.split('.').lastOption.getOrElse(name)) + + private def sanitizerProducedValueRef(prototype: PrototypeSummary, call: ResolvedCall): Option[String] = + (call.returnRefs ++ callTargetValueRef(prototype, call).toVector).find(_.nonEmpty) + + private def callTargetValueRef(prototype: PrototypeSummary, call: ResolvedCall): Option[String] = + prototype.instructions + .find(instruction => + instruction.pc == call.pc && (instruction.opcode == LuaOpcode.Call || instruction.opcode == LuaOpcode.TailCall) + ) + .map(instruction => s"${prototype.prototypeId}@pc${instruction.pc}:r${instruction.a}") + + private def sanitizerClassificationsFor( + taintPaths: Vector[LuaTaintPath], + sanitizerCalls: Vector[LuaSanitizerCall] + ): Vector[LuaSanitizerClassification] = + taintPaths.flatMap { path => + sanitizerCalls + .flatMap { call => + val onChain = path.pathSteps.contains(call.sanitizedValueRef) + val comparableCall = sameModuleRef(call.sanitizedValueRef, path.sourceRef) + val classification = if (onChain) "sanitized" else "not-sanitized" + Option.when(onChain || comparableCall)( + LuaSanitizerClassification( + path.sourceRef, + path.sinkRef, + call.callsiteId, + call.sanitizerName, + appliesToSink = true, + onDataflowChain = onChain, + classification + ) + ) + } + } + + private def reportClassificationsFor( + taintPaths: Vector[LuaTaintPath], + sanitizerClassifications: Vector[LuaSanitizerClassification] + ): Vector[LuaReportClassification] = + taintPaths.map { path => + val sanitized = sanitizerClassifications.exists(row => + row.sourceRef == path.sourceRef && row.sinkRef == path.sinkRef && row.onDataflowChain + ) + if (sanitized) { + LuaReportClassification(path.sourceRef, path.sinkRef, "sanitized", "on-chain-sanitizer") + } else { + LuaReportClassification(path.sourceRef, path.sinkRef, "true-positive", "no-on-chain-sanitizer") + } + } + + private def vulnerabilityReportsFor( + taintPaths: Vector[LuaTaintPath], + reportClassifications: Vector[LuaReportClassification] + ): Vector[LuaVulnerabilityReport] = + taintPaths.flatMap { path => + reportClassifications + .find(row => row.sourceRef == path.sourceRef && row.sinkRef == path.sinkRef) + .filter(_.classification == "true-positive") + .map(row => + LuaVulnerabilityReport( + path.sourceRef, + path.sinkRef, + "path-proven", + row.classification, + path.pathSteps, + path.provenance + ) + ) + } + + private def semanticBoundaries( + modules: Vector[ModuleSummary], + resolutions: Vector[LuaModuleResolution], + fieldTargets: Vector[LuaModuleFieldCallTarget] + ): Vector[LuaE4Boundary] = { + val resolutionBoundaries = resolutions.collect { + case resolution if resolution.resolutionStatus != "matched" => + LuaE4Boundary( + s"${resolution.fromModulePath}:require:${resolution.requireString}", + "module-resolution", + unresolvedReason(resolution) + ) + } + val unresolvedCalls = modules.flatMap { module => + module.unresolvedLocalCalls.map(call => + LuaE4Boundary(qualify(module.path, call.callsiteId), "interprocedural", "unresolved-callee") + ) + } + val fieldTargetCallsites = fieldTargets.map(_.callsiteId).toSet + val missingFields = modules.flatMap { module => + module.fieldCalls + .filter(call => !fieldTargetCallsites(call.callsiteId)) + .filter(call => module.requireResultRefs.keySet.exists(ref => call.requireRef.contains(ref))) + .map(call => LuaE4Boundary(qualify(module.path, call.callsiteId), "module-field", "missing-export-field")) + } + resolutionBoundaries ++ unresolvedCalls ++ missingFields + } + + private def qualify(modulePath: String, ref: String): String = s"$modulePath:$ref" + + private final case class ProgramArtifact(path: String, root: Option[LuaPrototype]) + + private final case class ModuleSummary( + path: String, + declaredModuleNames: Set[String], + prototypes: Vector[PrototypeSummary], + requireCalls: Vector[RequireCall], + requireResultRefs: Map[String, String], + capturedRequireRefs: Map[String, String], + exports: Vector[ModuleExport], + fieldCalls: Vector[FieldCall], + localCalls: Vector[LocalCall], + unresolvedLocalCalls: Vector[UnresolvedLocalCall], + localFlows: Vector[LuaLocalFlow], + globalFlows: Vector[LuaGlobalFlow] + ) { + def prototype(id: String): Option[PrototypeSummary] = prototypes.find(_.prototypeId == id) + } + + private object ModuleSummary { + def fromArtifact(artifact: ProgramArtifact): Option[ModuleSummary] = + artifact.root.map { root => + val capturedNames = capturedUpvalueNames(root) + val prototypes = allPrototypes(root).map(prototype => PrototypeSummary.fromPrototype(prototype, capturedNames)) + val declaredNames = declaredModuleNames(root) + val requireCalls = prototypes.flatMap(detectRequireCalls) + val exports = detectExports(artifact.path, root) + val localCalls = + detectLocalCalls(root) ++ detectPlainModuleGlobalExportCalls(artifact.path, prototypes, exports) + val localSemantic = LuaInstructionSemantics.normalize(root) + val capturedRefs = capturedRequireRefs(root, requireCalls) + val fieldCalls = + prototypes.flatMap(prototype => detectFieldCalls(artifact.path, prototype, requireCalls, capturedRefs)) + ModuleSummary( + path = artifact.path, + declaredModuleNames = declaredNames, + prototypes = prototypes, + requireCalls = requireCalls, + requireResultRefs = requireCalls.collect { + case call if call.resultRef.nonEmpty && call.requireString.exists(_.nonEmpty) => + call.resultRef.get -> modulePathForRequire(call.requireString.get) + }.toMap, + capturedRequireRefs = capturedRefs, + exports = exports, + fieldCalls = fieldCalls, + localCalls = localCalls, + unresolvedLocalCalls = localSemantic.unresolvedCalls.map(call => UnresolvedLocalCall(call.callsiteId)), + localFlows = localSemantic.localFlows, + globalFlows = localSemantic.globalFlows + ) + } + } + + private final case class PrototypeSummary( + prototypeId: String, + numParams: Int, + maxStack: Int, + instructions: Vector[LuaInstruction], + constants: Vector[LuaConstant], + parameterRefs: Vector[String], + returnRefs: Vector[String], + calls: Vector[ResolvedCall], + capturedNames: Map[Int, String] + ) + + private object PrototypeSummary { + def fromPrototype( + prototype: LuaPrototype, + capturedNamesByPrototype: Map[String, Map[Int, String]] = Map.empty + ): PrototypeSummary = { + val capturedNames = capturedNamesByPrototype.getOrElse(prototype.prototypeId, Map.empty) + PrototypeSummary( + prototype.prototypeId, + prototype.numParams, + prototype.maxStack, + prototype.instructions, + prototype.constants, + (0 until prototype.numParams).map(slot => s"${prototype.prototypeId}:r$slot").toVector, + returnRefs(prototype), + resolvedCalls(prototype, capturedNames), + capturedNames + ) + } + } + + private final case class RequireCall( + callsiteId: String, + prototypeId: String, + resultRef: Option[String], + requireString: Option[String] + ) + + private final case class ModuleExport( + modulePath: String, + tableRef: String, + fieldName: String, + targetPrototypeId: String + ) + + private final case class FieldCall( + callsiteId: String, + fieldName: String, + resolvedName: Option[String], + targetRef: Option[String], + requireRef: Option[String], + argumentRefs: Vector[String], + resultRef: Option[String] + ) + + private final case class RegisterFieldTarget( + fieldName: String, + resolvedName: Option[String], + targetRef: String, + requireRef: Option[String] + ) + + private final case class LocalCall( + callsiteId: String, + targetPrototypeId: String, + argumentRefs: Vector[String], + resultRef: Option[String] + ) + + private final case class UnresolvedLocalCall(callsiteId: String) + + private final case class ResolvedCall( + callsiteId: String, + prototypeId: String, + pc: Int, + resolvedName: Option[String], + argumentRefs: Vector[String], + returnRefs: Vector[String] + ) + + private final class ModuleIndex(modules: Vector[ModuleSummary]) { + private val byPath = modules.map(module => module.path -> module).toMap + private val byDeclaredName = + modules.flatMap(module => module.declaredModuleNames.map(name => name -> module)).groupMap(_._1)(_._2) + + def module(path: String): Option[ModuleSummary] = byPath.get(path) + + def resolve(requireString: String): ModuleResolutionResult = + if (requireString.isEmpty) ModuleResolutionResult.Unresolved + else { + val requiredSuffix = modulePathForRequire(requireString) + val pathCandidates = + modules.filter(module => module.path == requiredSuffix || module.path.endsWith(s"/$requiredSuffix")) + val candidates = (pathCandidates ++ byDeclaredName.getOrElse(requireString, Vector.empty)).distinct + .sortBy(module => (module.path.count(_ == '/'), module.path)) + candidates.headOption match { + case Some(module) => ModuleResolutionResult.Matched(module) + case None => ModuleResolutionResult.Unresolved + } + } + } + + private enum ModuleResolutionResult { + case Matched(module: ModuleSummary) + case Unresolved + } + + private def allPrototypes(prototype: LuaPrototype): Vector[LuaPrototype] = + prototype +: prototype.nested.flatMap(allPrototypes) + + private def detectRequireCalls(prototype: PrototypeSummary): Vector[RequireCall] = + prototype.instructions + .filter(instruction => instruction.opcode == LuaOpcode.Call && isGlobalName(prototype, instruction.a, "require")) + .map { instruction => + val argumentString = precedingLoadString(prototype, instruction.pc, instruction.a + 1) + RequireCall( + callsiteId = s"${prototype.prototypeId}@pc${instruction.pc}", + prototypeId = prototype.prototypeId, + resultRef = callReturnRefs(instruction).headOption.map(ref => s"${prototype.prototypeId}$ref"), + requireString = argumentString + ) + } + + private def detectExports(path: String, root: LuaPrototype): Vector[ModuleExport] = { + val returnedTableExports = root.instructions + .filter(instruction => instruction.opcode == LuaOpcode.SetTable) + .flatMap { instruction => + for { + fieldName <- constantName(root.constants, instruction.b - RkConstantBase) + .filter(_ => instruction.b >= RkConstantBase) + targetSlot <- instruction.c + target <- closureInSlotBefore(root, instruction.pc, targetSlot) + if isReturnedTable(root, instruction.a) + } yield ModuleExport(path, s"${root.prototypeId}:r${instruction.a}", fieldName, target) + } + returnedTableExports ++ detectPlainModuleGlobalExports(path, root) ++ + detectPlainModuleGlobalTableFieldExports(path, root) + } + + private def detectPlainModuleGlobalExports(path: String, root: LuaPrototype): Vector[ModuleExport] = + if (hasPlainModuleLiteralCall(root)) { + root.instructions + .filter(_.opcode == LuaOpcode.SetGlobal) + .flatMap { instruction => + for { + fieldName <- constantName(root.constants, instruction.b) + target <- closureInSlotBefore(root, instruction.pc, instruction.a) + } yield ModuleExport(path, s"$path:module-global", fieldName, target) + } + } else Vector.empty + + private def detectPlainModuleGlobalTableFieldExports(path: String, root: LuaPrototype): Vector[ModuleExport] = { + val summary = PrototypeSummary.fromPrototype(root) + val registerStrings = scala.collection.mutable.Map.empty[Int, String] + val registerClosures = scala.collection.mutable.Map.empty[Int, String] + val registerTables = scala.collection.mutable.Map.empty[Int, String] + val tableFields = scala.collection.mutable.Map.empty[String, scala.collection.mutable.Map[String, String]] + val globalTables = scala.collection.mutable.Map.empty[String, String] + val prototypeIds = allPrototypes(root).map(_.prototypeId).toSet + var moduleCallSeen = false + + def clearRegister(slot: Int): Unit = { + registerStrings -= slot + registerClosures -= slot + registerTables -= slot + } + + def copyRegister(dest: Int, source: Int): Unit = { + val stringValue = registerStrings.get(source) + val closureValue = registerClosures.get(source) + val tableValue = registerTables.get(source) + clearRegister(dest) + stringValue.foreach(registerStrings += dest -> _) + closureValue.foreach(registerClosures += dest -> _) + tableValue.foreach(registerTables += dest -> _) + } + + def clearCallWrites(instruction: LuaInstruction): Unit = + callReturnRefs(instruction).flatMap(registerWrite).foreach(write => clearRegister(write.slot)) + + def setTableKey(instruction: LuaInstruction): Option[String] = + if (instruction.b >= RkConstantBase) constantName(root.constants, instruction.b - RkConstantBase) + else registerStrings.get(instruction.b) + + def setTableClosureValue(instruction: LuaInstruction): Option[String] = + instruction.c.filter(_ < RkConstantBase).flatMap(registerClosures.get) + + root.instructions.sortBy(_.pc).foreach { + case instruction if instruction.opcode == LuaOpcode.LoadK => + clearRegister(instruction.a) + constantName(root.constants, instruction.b).foreach(registerStrings += instruction.a -> _) + + case instruction if instruction.opcode == LuaOpcode.Closure => + clearRegister(instruction.a) + registerClosures += instruction.a -> s"${root.prototypeId}.${instruction.b}" + + case instruction if instruction.opcode == LuaOpcode.NewTable => + clearRegister(instruction.a) + val tableRef = s"${root.prototypeId}@pc${instruction.pc}:r${instruction.a}" + registerTables += instruction.a -> tableRef + tableFields.getOrElseUpdate(tableRef, scala.collection.mutable.Map.empty) + + case instruction if instruction.opcode == LuaOpcode.Move => + copyRegister(instruction.a, instruction.b) + + case instruction if instruction.opcode == LuaOpcode.GetGlobal => + clearRegister(instruction.a) + constantName(root.constants, instruction.b).foreach { fieldName => + globalTables.get(fieldName).foreach(registerTables += instruction.a -> _) + } + + case instruction if instruction.opcode == LuaOpcode.SetGlobal => + for { + fieldName <- constantName(root.constants, instruction.b) + tableRef <- registerTables.get(instruction.a) + if moduleCallSeen + } globalTables += fieldName -> tableRef + + case instruction if instruction.opcode == LuaOpcode.SetTable => + for { + tableRef <- registerTables.get(instruction.a) + key <- setTableKey(instruction) + target <- setTableClosureValue(instruction) + } tableFields.getOrElseUpdate(tableRef, scala.collection.mutable.Map.empty) += key -> target + + case instruction if instruction.opcode == LuaOpcode.Call || instruction.opcode == LuaOpcode.TailCall => + if ( + summary.calls.exists(call => call.callsiteId == s"${root.prototypeId}@pc${instruction.pc}") && + callTargetNameAt(summary, instruction.pc, instruction.a).contains("module") && + precedingLoadString(summary, instruction.pc, instruction.a + 1).nonEmpty + ) { + moduleCallSeen = true + } + clearCallWrites(instruction) + + case instruction if instruction.opcode == LuaOpcode.Return || instruction.opcode == LuaOpcode.SetList => + + case instruction => + clearRegister(instruction.a) + } + + globalTables.toVector.flatMap { case (globalName, tableRef) => + tableFields.get(tableRef).toVector.flatMap { fields => + fields.toVector.collect { + case (fieldName, targetPrototypeId) if prototypeIds(targetPrototypeId) => + ModuleExport(path, s"$path:module-global", s"$globalName.$fieldName", targetPrototypeId) + } + } + }.distinct + } + + private def hasPlainModuleLiteralCall(root: LuaPrototype): Boolean = { + declaredModuleNames(root).nonEmpty + } + + private def declaredModuleNames(root: LuaPrototype): Set[String] = { + val summary = PrototypeSummary.fromPrototype(root) + summary.calls.flatMap { call => + if (call.resolvedName.contains("module")) { + call.argumentRefs.headOption + .flatMap(ref => registerWrite(ref)) + .flatMap(write => precedingStringConstant(summary, call.pc, write.slot)) + } else None + }.toSet + } + + private def detectLocalCalls(root: LuaPrototype): Vector[LocalCall] = { + val semantics = LuaInstructionSemantics.normalize(root) + val closureCalls = semantics.callTargetCandidates.flatMap { candidate => + semantics.callSites + .find(_.callsiteId == candidate.callsiteId) + .map { callsite => + LocalCall( + callsiteId = callsite.callsiteId, + targetPrototypeId = candidate.targetRef, + argumentRefs = callsite.firstArgSlot + .zip(callsite.argCount) + .toVector + .flatMap { case (firstSlot, count) => + (firstSlot until (firstSlot + count)).map(slot => s"${callsite.prototypeId}@pc${callsite.pc}:r$slot") + }, + resultRef = callsite.firstReturnSlot.map(slot => s"${callsite.prototypeId}@pc${callsite.pc}:r$slot") + ) + } + } + closureCalls.distinct + } + + private def detectPlainModuleGlobalExportCalls( + path: String, + prototypes: Vector[PrototypeSummary], + exports: Vector[ModuleExport] + ): Vector[LocalCall] = { + val uniquePlainGlobalExports = exports + .filter(_.tableRef == s"$path:module-global") + .groupBy(_.fieldName) + .collect { + case (fieldName, exportsForField) if exportsForField.size == 1 => + fieldName -> exportsForField.head.targetPrototypeId + } + .toMap + + prototypes.flatMap { prototype => + prototype.calls.flatMap { call => + for { + resolvedName <- call.resolvedName + targetPrototypeId <- uniquePlainGlobalExports.get(resolvedName) + } yield LocalCall( + callsiteId = call.callsiteId, + targetPrototypeId = targetPrototypeId, + argumentRefs = call.argumentRefs, + resultRef = call.returnRefs.headOption + ) + } + }.distinct + } + + private def detectFieldCalls( + path: String, + prototype: PrototypeSummary, + requireCalls: Vector[RequireCall], + capturedRequireRefs: Map[String, String] + ): Vector[FieldCall] = { + val requireCallsByCallsite = requireCalls.map(call => call.callsiteId -> call).toMap + val requireStringByRef = requireCalls + .flatMap(call => call.resultRef.zip(call.requireString)) + .toMap + val registerNames = scala.collection.mutable.Map.empty[Int, Vector[String]] + val registerRequireRefs = scala.collection.mutable.Map.empty[Int, String] + val registerFieldTargets = scala.collection.mutable.Map.empty[Int, RegisterFieldTarget] + val rows = Vector.newBuilder[FieldCall] + + def clearRegister(slot: Int): Unit = { + registerNames -= slot + registerRequireRefs -= slot + registerFieldTargets -= slot + } + + def clearCallWrites(instruction: LuaInstruction): Unit = + callReturnRefs(instruction).flatMap(registerWrite).foreach(write => clearRegister(write.slot)) + + def clearDefaultWrite(instruction: LuaInstruction): Unit = + instruction.opcode match { + case LuaOpcode.SetGlobal | LuaOpcode.SetTable | LuaOpcode.SetList | LuaOpcode.Return | LuaOpcode.Eq | + LuaOpcode.Lt | LuaOpcode.Le | LuaOpcode.Test | LuaOpcode.Jmp => + case LuaOpcode.Call | LuaOpcode.TailCall => + clearCallWrites(instruction) + case _ => + clearRegister(instruction.a) + } + + def copyRegister(dest: Int, source: Int): Unit = { + clearRegister(dest) + registerNames.get(source).foreach(registerNames += dest -> _) + registerRequireRefs.get(source).foreach(registerRequireRefs += dest -> _) + registerFieldTargets.get(source).foreach(registerFieldTargets += dest -> _) + } + + prototype.instructions.sortBy(_.pc).foreach { + case instruction if instruction.opcode == LuaOpcode.GetGlobal => + clearRegister(instruction.a) + constantName(prototype.constants, instruction.b).foreach(name => registerNames += instruction.a -> Vector(name)) + + case instruction if instruction.opcode == LuaOpcode.GetUpval => + clearRegister(instruction.a) + capturedRequireRefs.get(capturedRequireRefKey(prototype.prototypeId, instruction.b)).foreach { ref => + registerRequireRefs += instruction.a -> ref + requireStringByRef.get(ref).foreach(name => registerNames += instruction.a -> Vector(name)) + } + prototype.capturedNames.get(instruction.b).foreach(name => registerNames += instruction.a -> Vector(name)) + + case instruction if instruction.opcode == LuaOpcode.Move => + copyRegister(instruction.a, instruction.b) + + case instruction if instruction.opcode == LuaOpcode.GetTable => + val key = fieldName(prototype.constants, instruction) + val baseName = registerNames.get(instruction.b) + val baseTarget = registerFieldTargets.get(instruction.b) + val requireRef = registerRequireRefs.get(instruction.b).orElse(baseTarget.flatMap(_.requireRef)) + clearRegister(instruction.a) + key.foreach { field => + val resolvedName = baseName.map(parts => (parts :+ field).mkString(".")) + baseName.foreach(parts => registerNames += instruction.a -> (parts :+ field)) + requireRef.foreach(registerRequireRefs += instruction.a -> _) + if (requireRef.nonEmpty) { + val exportField = baseTarget.map(target => s"${target.fieldName}.$field").getOrElse(field) + registerFieldTargets += instruction.a -> RegisterFieldTarget( + exportField, + resolvedName, + s"${prototype.prototypeId}@pc${instruction.pc}:r${instruction.a}", + requireRef + ) + } + } + + case instruction if instruction.opcode == LuaOpcode.Self => + val key = instruction.c.flatMap(value => constantName(prototype.constants, value - RkConstantBase)) + val baseName = registerNames.get(instruction.b) + val baseTarget = registerFieldTargets.get(instruction.b) + val requireRef = registerRequireRefs.get(instruction.b).orElse(baseTarget.flatMap(_.requireRef)) + clearRegister(instruction.a) + copyRegister(instruction.a + 1, instruction.b) + key.foreach { field => + val resolvedName = baseName.map(parts => (parts :+ field).mkString(".")) + baseName.foreach(parts => registerNames += instruction.a -> (parts :+ field)) + requireRef.foreach(registerRequireRefs += instruction.a -> _) + if (requireRef.nonEmpty) { + val exportField = baseTarget.map(target => s"${target.fieldName}.$field").getOrElse(field) + registerFieldTargets += instruction.a -> RegisterFieldTarget( + exportField, + resolvedName, + s"${prototype.prototypeId}@pc${instruction.pc}:r${instruction.a}", + requireRef + ) + } + } + + case instruction if instruction.opcode == LuaOpcode.Call || instruction.opcode == LuaOpcode.TailCall => + val callsiteId = s"${prototype.prototypeId}@pc${instruction.pc}" + registerFieldTargets.get(instruction.a).foreach { target => + rows += FieldCall( + callsiteId, + target.fieldName, + target.resolvedName, + Some(target.targetRef), + target.requireRef, + callArgumentRefs(prototype, instruction), + callReturnRefs(instruction).headOption.map(ref => s"${prototype.prototypeId}$ref") + ) + } + val requireCall = requireCallsByCallsite.get(callsiteId).filter { call => + call.requireString.nonEmpty && callTargetNameAt(prototype, instruction.pc, instruction.a).contains("require") + } + clearCallWrites(instruction) + requireCall.foreach { call => + call.resultRef.foreach { ref => + registerWrite(ref) + .filter(_.slot == instruction.a) + .foreach { _ => + registerRequireRefs += instruction.a -> ref + call.requireString.foreach(name => registerNames += instruction.a -> Vector(name)) + } + } + } + + case instruction + if instruction.opcode == LuaOpcode.LoadK || instruction.opcode == LuaOpcode.LoadBool || + instruction.opcode == LuaOpcode.LoadNil || instruction.opcode == LuaOpcode.NewTable || + instruction.opcode == LuaOpcode.Closure || instruction.opcode == LuaOpcode.Vararg => + clearRegister(instruction.a) + + case instruction => + clearDefaultWrite(instruction) + } + + val statefulRows = rows.result() + val directRows = prototype.instructions + .filter(instruction => instruction.opcode == LuaOpcode.Call || instruction.opcode == LuaOpcode.TailCall) + .flatMap { instruction => + precedingGetTable(prototype, instruction.pc, instruction.a).flatMap { getTable => + fieldName(prototype.constants, getTable).map { field => + val baseName = callTargetNameAt(prototype, getTable.pc, getTable.b) + FieldCall( + callsiteId = s"${prototype.prototypeId}@pc${instruction.pc}", + fieldName = field, + resolvedName = baseName.map(name => s"$name.$field"), + targetRef = Some(s"${prototype.prototypeId}@pc${getTable.pc}:r${getTable.a}"), + requireRef = requireRefForGetTable(prototype, getTable, requireCalls, capturedRequireRefs), + argumentRefs = callArgumentRefs(prototype, instruction), + resultRef = callReturnRefs(instruction).headOption.map(ref => s"${prototype.prototypeId}$ref") + ) + } + } + } + (statefulRows ++ directRows).distinct + } + + private def requireRefForGetTable( + prototype: PrototypeSummary, + getTable: LuaInstruction, + requireCalls: Vector[RequireCall], + capturedRequireRefs: Map[String, String] + ): Option[String] = { + getUpvalueBefore(prototype, getTable.pc, getTable.b) match { + case Some(upvalueSlot) => + capturedRequireRefs.get(capturedRequireRefKey(prototype.prototypeId, upvalueSlot)) + case None => + localRequireRefForGetTable(prototype, getTable, requireCalls) + } + } + + private def localRequireRefForGetTable( + prototype: PrototypeSummary, + getTable: LuaInstruction, + requireCalls: Vector[RequireCall] + ): Option[String] = + requireCalls + .filter(_.prototypeId == prototype.prototypeId) + .flatMap(_.resultRef) + .flatMap(ref => registerWrite(ref).map(write => write -> ref)) + .filter { case (write, _) => write.slot == getTable.b && write.pc < getTable.pc } + .sortBy { case (write, _) => write.pc } + .lastOption + .map { case (_, ref) => ref } + + private def capturedRequireRefs(root: LuaPrototype, requireCalls: Vector[RequireCall]): Map[String, String] = { + def collect(prototype: LuaPrototype, inheritedUpvalueRefs: Map[Int, String]): Map[String, String] = { + val registerRequireRefs = scala.collection.mutable.Map.empty[Int, (Int, String)] + val captured = scala.collection.mutable.Map.empty[String, String] + val sorted = prototype.instructions.sortBy(_.pc) + val bindingPcs = closureBindingPcs(prototype) + + def clearSlot(slot: Int): Unit = + registerRequireRefs -= slot + + def newestRequireRef(slot: Int, pc: Int): Option[String] = + registerRequireRefs + .get(slot) + .filter(_._1 < pc) + .map(_._2) + + def boundLocalRequireRef(slot: Int, closurePc: Int): Option[String] = + requireCalls + .filter(call => call.prototypeId == prototype.prototypeId && call.requireString.nonEmpty) + .flatMap(call => call.resultRef.flatMap(ref => registerWrite(ref).map(write => (write, ref)))) + .filter { case (write, _) => + write.slot == slot && + write.pc < closurePc && + !hasRuntimeOverwrite(slot, write.pc, closurePc, sorted, bindingPcs) + } + .sortBy { case (write, _) => write.pc } + .lastOption + .map { case (_, ref) => ref } + + def childUpvalueRefs(closure: LuaInstruction): Map[Int, String] = { + val childPrototypeId = s"${prototype.prototypeId}.${closure.b}" + prototype.nested + .find(_.prototypeId == childPrototypeId) + .map { child => + sorted + .dropWhile(_.pc <= closure.pc) + .take(effectiveUpvalueCount(child)) + .zipWithIndex + .flatMap { + case (binder, upvalueSlot) if binder.opcode == LuaOpcode.Move => + boundLocalRequireRef(binder.b, closure.pc).map(upvalueSlot -> _) + case (binder, upvalueSlot) if binder.opcode == LuaOpcode.GetUpval => + inheritedUpvalueRefs.get(binder.b).map(upvalueSlot -> _) + case _ => None + } + .toMap + } + .getOrElse(Map.empty) + } + + sorted.foreach { + case instruction if bindingPcs(instruction.pc) => + case instruction if instruction.opcode == LuaOpcode.Call => + val callsiteId = s"${prototype.prototypeId}@pc${instruction.pc}" + val resolvedRequire = requireCalls + .find(call => + call.prototypeId == prototype.prototypeId && + call.callsiteId == callsiteId && + call.requireString.nonEmpty + ) + .flatMap(_.resultRef) + clearSlot(instruction.a) + resolvedRequire.foreach(ref => registerRequireRefs += instruction.a -> (instruction.pc -> ref)) + case instruction if instruction.opcode == LuaOpcode.GetUpval => + val inherited = inheritedUpvalueRefs.get(instruction.b) + clearSlot(instruction.a) + inherited.foreach(ref => registerRequireRefs += instruction.a -> (instruction.pc -> ref)) + case instruction if instruction.opcode == LuaOpcode.Move => + val moved = newestRequireRef(instruction.b, instruction.pc) + clearSlot(instruction.a) + moved.foreach(ref => registerRequireRefs += instruction.a -> (instruction.pc -> ref)) + case instruction if instruction.opcode == LuaOpcode.Closure => + clearSlot(instruction.a) + val childPrototypeId = s"${prototype.prototypeId}.${instruction.b}" + val childRefs = childUpvalueRefs(instruction) + childRefs.foreach { case (upvalueSlot, ref) => + captured += capturedRequireRefKey(childPrototypeId, upvalueSlot) -> ref + } + prototype.nested + .find(_.prototypeId == childPrototypeId) + .foreach(child => captured ++= collect(child, childRefs)) + case instruction + if instruction.opcode == LuaOpcode.LoadK || instruction.opcode == LuaOpcode.LoadBool || + instruction.opcode == LuaOpcode.LoadNil || instruction.opcode == LuaOpcode.GetGlobal || + instruction.opcode == LuaOpcode.GetTable || instruction.opcode == LuaOpcode.NewTable || + instruction.opcode == LuaOpcode.Self || instruction.opcode == LuaOpcode.Vararg || + instruction.opcode == LuaOpcode.TailCall => + clearSlot(instruction.a) + case _ => + } + + captured.toMap + } + + collect(root, Map.empty) + } + + private def hasRuntimeOverwrite( + slot: Int, + fromPc: Int, + toPc: Int, + sortedInstructions: Vector[LuaInstruction], + bindingPcs: Set[Int] + ): Boolean = + sortedInstructions.exists { instruction => + instruction.pc > fromPc && + instruction.pc < toPc && + !bindingPcs(instruction.pc) && + !(instruction.opcode == LuaOpcode.Move && instruction.a == slot && instruction.b == slot) && + overwritesSlot(instruction, slot) + } + + private def parameterFlowsToCallTarget(prototype: PrototypeSummary, parameterSlot: Int): Boolean = { + var aliases = Set(parameterSlot) + prototype.instructions.sortBy(_.pc).exists { instruction => + val isTarget = + (instruction.opcode == LuaOpcode.Call || instruction.opcode == LuaOpcode.TailCall) && aliases(instruction.a) + if (!isTarget) { + instruction.opcode match { + case LuaOpcode.Move => + val sourceIsAlias = aliases(instruction.b) + if (aliases(instruction.a)) { + aliases -= instruction.a + } + if (sourceIsAlias) { + aliases += instruction.a + } + case LuaOpcode.LoadK | LuaOpcode.LoadBool | LuaOpcode.LoadNil | LuaOpcode.GetUpval | LuaOpcode.GetGlobal | + LuaOpcode.GetTable | LuaOpcode.NewTable | LuaOpcode.Self | + LuaOpcode.Closure | LuaOpcode.Vararg | LuaOpcode.Call | LuaOpcode.TailCall => + aliases -= instruction.a + case _ => + } + } + isTarget + } + } + + private final case class RegisterWrite(pc: Int, slot: Int) + + private def registerWrite(ref: String): Option[RegisterWrite] = + ref.split("@pc", 2).lift(1).flatMap { afterPc => + afterPc.split(":r", 2).toList match { + case pcText :: slotText :: Nil => + pcText.toIntOption.zip(slotText.toIntOption).map((pc, slot) => RegisterWrite(pc, slot)) + case _ => None + } + } + + private def getUpvalueBefore(prototype: PrototypeSummary, pc: Int, slot: Int): Option[Int] = + prototype.instructions + .filter(instruction => instruction.pc < pc && instruction.a == slot && instruction.opcode == LuaOpcode.GetUpval) + .lastOption + .map(_.b) + + private def capturedRequireRefKey(prototypeId: String, upvalueSlot: Int): String = s"$prototypeId:u$upvalueSlot" + + private def capturedUpvalueNames(root: LuaPrototype): Map[String, Map[Int, String]] = { + def collect(prototype: LuaPrototype, currentUpvalueNames: Map[Int, String]): Map[String, Map[Int, String]] = { + val namesBySlot = scala.collection.mutable.Map.empty[Int, String] + val capturedByPrototype = scala.collection.mutable.Map.empty[String, Map[Int, String]] + val bindingPcs = closureBindingPcs(prototype) + def clearWrittenSlot(instruction: LuaInstruction): Unit = + namesBySlot -= instruction.a + def childUpvalueNames(closure: LuaInstruction): Map[Int, String] = { + val childPrototypeId = s"${prototype.prototypeId}.${closure.b}" + prototype.nested + .find(_.prototypeId == childPrototypeId) + .map { child => + prototype.instructions + .filter(_.pc > closure.pc) + .sortBy(_.pc) + .take(effectiveUpvalueCount(child)) + .zipWithIndex + .flatMap { + case (binder, upvalueSlot) if binder.opcode == LuaOpcode.Move => + namesBySlot.get(binder.b).map(name => upvalueSlot -> name) + case (binder, upvalueSlot) if binder.opcode == LuaOpcode.GetUpval => + currentUpvalueNames.get(binder.b).map(name => upvalueSlot -> name) + case _ => None + } + .toMap + } + .getOrElse(Map.empty) + } + + prototype.instructions.sortBy(_.pc).foreach { + case instruction if bindingPcs(instruction.pc) => + case instruction if instruction.opcode == LuaOpcode.GetGlobal => + clearWrittenSlot(instruction) + constantName(prototype.constants, instruction.b).foreach(name => namesBySlot += instruction.a -> name) + case instruction if instruction.opcode == LuaOpcode.GetUpval => + clearWrittenSlot(instruction) + currentUpvalueNames.get(instruction.b).foreach(name => namesBySlot += instruction.a -> name) + case instruction if instruction.opcode == LuaOpcode.GetTable => + val resolved = for { + base <- namesBySlot.get(instruction.b) + field <- fieldName(prototype.constants, instruction) + } yield s"$base.$field" + clearWrittenSlot(instruction) + resolved.foreach(name => namesBySlot += instruction.a -> name) + case instruction if instruction.opcode == LuaOpcode.Move => + val resolved = namesBySlot.get(instruction.b) + clearWrittenSlot(instruction) + resolved.foreach(name => namesBySlot += instruction.a -> name) + case instruction if instruction.opcode == LuaOpcode.Closure => + val childPrototypeId = s"${prototype.prototypeId}.${instruction.b}" + val childNames = childUpvalueNames(instruction) + capturedByPrototype += childPrototypeId -> childNames + prototype.nested + .find(_.prototypeId == childPrototypeId) + .foreach(child => capturedByPrototype ++= collect(child, childNames)) + case instruction if instruction.opcode == LuaOpcode.Call => + val resolved = for { + targetName <- namesBySlot.get(instruction.a) + if targetName == "require" + if instruction.c.forall(_ != 1) + moduleName <- precedingLoadString(prototype, instruction.pc, instruction.a + 1) + } yield moduleName + clearWrittenSlot(instruction) + resolved.foreach(name => namesBySlot += instruction.a -> name) + case instruction if instruction.opcode == LuaOpcode.LoadK => + clearWrittenSlot(instruction) + case instruction + if instruction.opcode == LuaOpcode.LoadBool || instruction.opcode == LuaOpcode.LoadNil || + instruction.opcode == LuaOpcode.NewTable || instruction.opcode == LuaOpcode.Vararg || + instruction.opcode == LuaOpcode.TailCall => + clearWrittenSlot(instruction) + case instruction + if instruction.opcode != LuaOpcode.SetGlobal && instruction.opcode != LuaOpcode.SetTable && + instruction.opcode != LuaOpcode.SetUpval && instruction.opcode != LuaOpcode.Return => + clearWrittenSlot(instruction) + case _ => + } + + capturedByPrototype.toMap + } + + collect(root, Map.empty) + } + + private def closureBindingPcs(prototype: LuaPrototype): Set[Int] = + prototype.instructions + .sortBy(_.pc) + .zipWithIndex + .flatMap { + case (closure, index) if closure.opcode == LuaOpcode.Closure => + prototype.nested.find(_.prototypeId == s"${prototype.prototypeId}.${closure.b}").toVector.flatMap { child => + (1 to effectiveUpvalueCount(child)) + .takeWhile { offset => + prototype.instructions + .sortBy(_.pc) + .lift(index + offset) + .exists(binding => + binding.pc == closure.pc + offset && + (binding.opcode == LuaOpcode.Move || binding.opcode == LuaOpcode.GetUpval) + ) + } + .flatMap(offset => prototype.instructions.sortBy(_.pc).lift(index + offset).map(_.pc)) + } + case _ => Vector.empty + } + .toSet + + private def effectiveUpvalueCount(prototype: LuaPrototype): Int = { + val usedUpvalueCount = prototype.instructions + .collect { + case instruction if instruction.opcode == LuaOpcode.GetUpval || instruction.opcode == LuaOpcode.SetUpval => + instruction.b + 1 + } + .maxOption + .getOrElse(0) + prototype.upvalueCount.max(usedUpvalueCount) + } + + private def returnRefs(prototype: LuaPrototype): Vector[String] = { + val explicitReturns = prototype.instructions + .filter(_.opcode == LuaOpcode.Return) + .flatMap { instruction => + val slots = instruction.b match { + case 0 => Vector.empty + case 1 => Vector.empty + case n => (instruction.a until (instruction.a + n - 1)).toVector + } + slots.map(slot => s"${prototype.prototypeId}@pc${instruction.pc}:r$slot") + } + val tailCallReturns = prototype.instructions + .filter(_.opcode == LuaOpcode.TailCall) + .map(instruction => s"${prototype.prototypeId}@pc${instruction.pc}:r${instruction.a}") + explicitReturns ++ tailCallReturns + } + + private def resolvedCalls( + prototype: LuaPrototype, + capturedNames: Map[Int, String] = Map.empty + ): Vector[ResolvedCall] = { + val summary = PrototypeSummary( + prototype.prototypeId, + prototype.numParams, + prototype.maxStack, + prototype.instructions, + prototype.constants, + Vector.empty, + Vector.empty, + Vector.empty, + capturedNames + ) + prototype.instructions + .filter(instruction => instruction.opcode == LuaOpcode.Call || instruction.opcode == LuaOpcode.TailCall) + .map(instruction => + ResolvedCall( + callsiteId = s"${prototype.prototypeId}@pc${instruction.pc}", + prototypeId = prototype.prototypeId, + pc = instruction.pc, + resolvedName = resolvedCallName(summary, instruction), + argumentRefs = callArgumentRefs(prototype, instruction), + returnRefs = callReturnRefs(instruction).map(ref => s"${prototype.prototypeId}$ref") + ) + ) + } + + private def resolvedCallName(prototype: PrototypeSummary, call: LuaInstruction): Option[String] = + callTargetNameAt(prototype, call.pc, call.a) + + private def callTargetNameAt(prototype: PrototypeSummary, pc: Int, slot: Int): Option[String] = + prototype.instructions + .filter(instruction => instruction.pc < pc && instruction.a == slot) + .sortBy(_.pc) + .lastOption + .flatMap { + case instruction if instruction.opcode == LuaOpcode.GetGlobal => + constantName(prototype.constants, instruction.b) + case instruction if instruction.opcode == LuaOpcode.GetUpval => + prototype.capturedNames.get(instruction.b) + case instruction if instruction.opcode == LuaOpcode.GetTable => + for { + baseName <- callTargetNameAt(prototype, instruction.pc, instruction.b) + field <- fieldName(prototype.constants, instruction) + } yield s"$baseName.$field" + case instruction if instruction.opcode == LuaOpcode.Call && instruction.c.forall(_ != 1) => + for { + targetName <- callTargetNameAt(prototype, instruction.pc, instruction.a) + if targetName == "require" + moduleName <- precedingLoadString(prototype, instruction.pc, instruction.a + 1) + } yield moduleName + case _ => None + } + + private def precedingGlobalLoadName(prototype: PrototypeSummary, pc: Int, slot: Int): Option[String] = + prototype.instructions + .filter(instruction => instruction.pc < pc && instruction.a == slot && instruction.opcode == LuaOpcode.GetGlobal) + .lastOption + .flatMap(instruction => constantName(prototype.constants, instruction.b)) + + private def isGlobalName(prototype: PrototypeSummary, slot: Int, name: String): Boolean = + prototype.instructions.exists(instruction => + instruction.opcode == LuaOpcode.GetGlobal && + instruction.a == slot && + constantName(prototype.constants, instruction.b).contains(name) + ) + + private def precedingLoadString(prototype: PrototypeSummary, pc: Int, slot: Int): Option[String] = + prototype.instructions + .filter(instruction => instruction.pc < pc && instruction.a == slot && instruction.opcode == LuaOpcode.LoadK) + .lastOption + .flatMap(instruction => constantName(prototype.constants, instruction.b)) + + private def precedingLoadString(prototype: LuaPrototype, pc: Int, slot: Int): Option[String] = + prototype.instructions + .filter(instruction => instruction.pc < pc && instruction.a == slot && instruction.opcode == LuaOpcode.LoadK) + .lastOption + .flatMap(instruction => constantName(prototype.constants, instruction.b)) + + private def precedingStringConstant(prototype: PrototypeSummary, pc: Int, slot: Int): Option[String] = + precedingLoadString(prototype, pc, slot) + + private def closureInSlotBefore(prototype: LuaPrototype, pc: Int, slot: Int): Option[String] = + prototype.instructions + .filter(instruction => instruction.pc < pc && instruction.a == slot && instruction.opcode == LuaOpcode.Closure) + .lastOption + .map(instruction => s"${prototype.prototypeId}.${instruction.b}") + + private def isReturnedTable(root: LuaPrototype, slot: Int): Boolean = + root.instructions.exists(instruction => + instruction.opcode == LuaOpcode.Return && instruction.a == slot && instruction.b == 2 + ) + + private def precedingGetTable(prototype: PrototypeSummary, pc: Int, slot: Int): Option[LuaInstruction] = + prototype.instructions + .filter(instruction => instruction.pc < pc && overwritesSlot(instruction, slot)) + .sortBy(_.pc) + .lastOption + .filter(_.opcode == LuaOpcode.GetTable) + + private def fieldName(constants: Vector[LuaConstant], instruction: LuaInstruction): Option[String] = + instruction.c.filter(_ >= RkConstantBase).flatMap(value => constantName(constants, value - RkConstantBase)) + + private def callArgumentRefs(prototype: LuaPrototype, instruction: LuaInstruction): Vector[String] = + callArgumentRefs(prototype.prototypeId, prototype.maxStack, instruction) + + private def callArgumentRefs(prototype: PrototypeSummary, instruction: LuaInstruction): Vector[String] = + callArgumentRefs(prototype.prototypeId, prototype.maxStack, instruction) + + private def callArgumentRefs(prototypeId: String, maxStack: Int, instruction: LuaInstruction): Vector[String] = { + val slots = instruction.b match { + case 0 => ((instruction.a + 1) until maxStack).toVector + case 1 => Vector.empty + case n => ((instruction.a + 1) until (instruction.a + n)).toVector + } + slots.map(slot => s"$prototypeId@pc${instruction.pc}:r$slot") + } + + private def callReturnRefs(instruction: LuaInstruction): Vector[String] = { + val slots = instruction.c match { + case Some(0) => Vector(instruction.a) + case Some(1) => Vector.empty + case Some(n) => (instruction.a until (instruction.a + n - 1)).toVector + case None => Vector.empty + } + slots.map(slot => s"@pc${instruction.pc}:r$slot") + } + + private def isConcreteStringArgument(prototype: PrototypeSummary, callPc: Int, argumentRef: String): Boolean = + registerWrite(argumentRef).exists(write => + prototype.instructions + .filter(instruction => instruction.pc < callPc && instruction.a == write.slot) + .sortBy(_.pc) + .lastOption + .exists(instruction => + instruction.opcode == LuaOpcode.LoadK && + constantName(prototype.constants, instruction.b).nonEmpty + ) + ) + + private def triggerMatches(pattern: String, name: String): Boolean = + pattern.split('.').lastOption.contains(finalSegment(name)) + + private def finalSegment(name: String): String = + name.split('.').lastOption match { + case Some(segment) if segment.nonEmpty => segment + case _ => name + } + + private def sameModuleRef(left: String, right: String): Boolean = + left.split(':').headOption.nonEmpty && left.split(':').headOption == right.split(':').headOption + + private final case class QualifiedValueRef(modulePath: String, prototypeId: String, pc: Int, localRef: String) + + private def parseQualifiedValueRef(ref: String): QualifiedValueRef = { + val splitAt = Vector(".luac:") + .flatMap { marker => + val index = ref.indexOf(marker) + if (index >= 0) Some(index + marker.length - 1) else None + } + .headOption + .getOrElse(throw new IllegalArgumentException(s"Lua qualified ref is missing module path: $ref")) + val modulePath = ref.substring(0, splitAt) + val localRef = ref.substring(splitAt + 1) + val prototypeId = localRef + .split("@pc", 2) + .headOption + .getOrElse(throw new IllegalArgumentException(s"Lua value ref is missing prototype id: $ref")) + val pc = localRef + .split("@pc", 2) + .lift(1) + .flatMap(_.split(":r", 2).headOption) + .flatMap(_.toIntOption) + .getOrElse(throw new IllegalArgumentException(s"Lua value ref is missing pc: $ref")) + QualifiedValueRef(modulePath, prototypeId, pc, localRef) + } + + private def slotFromValueRef(ref: String): Int = + localRefFromQualifiedRef(ref) + .split(":r", 2) + .lift(1) + .flatMap(_.toIntOption) + .getOrElse(throw new IllegalArgumentException(s"Lua value ref is missing slot: $ref")) + + private def modulePathFromQualifiedRef(ref: String): String = { + val splitAt = Vector(".luac:") + .flatMap { marker => + val index = ref.indexOf(marker) + if (index >= 0) Some(index + marker.length - 1) else None + } + .headOption + .getOrElse(throw new IllegalArgumentException(s"Lua qualified ref is missing module path: $ref")) + ref.substring(0, splitAt) + } + + private def callsiteIdFromValueRef(ref: String): String = + ref.split(":r", 2).headOption.getOrElse(ref) + + private def callsitePc(callsiteId: String): Option[Int] = + callsiteId.indexOf("@pc") match { + case -1 => None + case index => callsiteId.substring(index + 3).toIntOption + } + + private def requiredCallsitePc(callsiteId: String): Int = + callsitePc(callsiteId).getOrElse(throw new IllegalStateException(s"missing callsite pc: callsite_id=$callsiteId")) + + private def prototypeIdFromCallsiteId(ref: String): String = + ref.split("@pc", 2).headOption.getOrElse(ref) + + private def localRefFromQualifiedRef(ref: String): String = { + val splitAt = Vector(".luac:") + .flatMap { marker => + val index = ref.indexOf(marker) + if (index >= 0) Some(index + marker.length - 1) else None + } + .headOption + .getOrElse(throw new IllegalArgumentException(s"Lua qualified ref is missing module path: $ref")) + ref.substring(splitAt + 1) + } + + private def constantName(constants: Vector[LuaConstant], index: Int): Option[String] = + constants.collectFirst { case LuaConstant(`index`, "string", LuaConstantValue.StringValue(value)) => + value + } + + private def modulePathForRequire(requireString: String): String = { + val parts = requireString.split('.').filter(_.nonEmpty) + if (parts.isEmpty) throw new IllegalArgumentException("empty Lua require string") + s"${parts.mkString("/")}.luac" + } + + private def moduleNamesForPath(modulePath: String): Set[String] = { + val path = modulePath.stripSuffix(".luac").stripSuffix(".lua") + val parts = path.split('/').filter(_.nonEmpty).toVector + val allSuffixNames = parts.indices.map(index => parts.drop(index).mkString(".")).filter(_.nonEmpty).toSet + val bareName = parts.lastOption.toSet + allSuffixNames ++ bareName + } + + private def modulePathDeclaresRequire(modulePath: String, requireString: String): Boolean = + moduleNamesForPath(modulePath).contains(requireString) + + private def unresolvedReason(resolution: LuaModuleResolution): String = + resolution.unresolvedReason match { + case Some(reason) => reason + case None => + throw new IllegalStateException( + s"non-matched Lua module resolution without unresolved reason at ${resolution.fromModulePath}:${resolution.requireCallsiteId}" + ) + } +} diff --git a/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/bytecode/LuaRealFirmwareEvidenceExporter.scala b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/bytecode/LuaRealFirmwareEvidenceExporter.scala new file mode 100644 index 000000000000..31da8e636ac3 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/bytecode/LuaRealFirmwareEvidenceExporter.scala @@ -0,0 +1,1034 @@ +package io.joern.lua2cpg.bytecode + +import io.joern.lua2cpg.Config +import io.joern.lua2cpg.passes.LuaBytecodeModelPass.DecodedBytecode + +import java.nio.charset.StandardCharsets +import java.nio.file.{Files, Path, Paths} +import scala.jdk.CollectionConverters.* + +object LuaRealFirmwareEvidenceExporter { + private val MaxRetainedPairProfileBytes = 4096L + + def write(config: Config, decoded: Vector[DecodedBytecode], semantics: LuaProgramSemantics): Unit = + config.realFirmwareOutputDir.foreach { outputDir => + validateTaintPathEndpoints(semantics) + val profile = pathSearchProfile(semantics) + val root = Paths.get(outputDir) + val stagingDir = root.resolve("staging") + Files.createDirectories(stagingDir) + + val artifacts = decoded.map { item => + val staging = stagingFor(item, semantics) + writeJson(stagingDir.resolve(s"${safeArtifactName(artifactIdFor(item.relativeName))}.json"), staging) + artifactSummary(item) + } + + writeJson( + root.resolve("decoder-summary.json"), + ujson.Obj( + "run_id" -> "lua2cpg-upstream-export", + "totals" -> ujson.Obj( + "input_count" -> decoded.size, + "decoded_count" -> decoded.count(_.result.artifact.accepted), + "diagnostic_count" -> decoded.count(!_.result.artifact.accepted), + "prototype_count" -> decoded.flatMap(_.result.root.toVector.flatMap(allPrototypes)).size, + "instruction_count" -> decoded + .flatMap(_.result.root.toVector.flatMap(allPrototypes)) + .map(_.instructions.size) + .sum, + "callsite_count" -> decoded.flatMap(_.result.root.toVector.flatMap(allPrototypes)).map(countCallsites).sum, + "flow_edge_count" -> decoded.map(item => localSemantics(item.result).localFlows.size).sum + ), + "artifacts" -> artifacts + ) + ) + writeJson(root.resolve("path-search-profile.json"), profile) + writeJson( + root.resolve("run-summary.json"), + ujson.Obj( + "run_id" -> "lua2cpg-upstream-export", + "status" -> "completed", + "native_status" -> "cpg-written", + "d19_path_parity_status" -> "not-run", + "native_d19_path_parity_status" -> "not-run", + "totals" -> ujson.Obj( + "input_count" -> decoded.size, + "decoded_count" -> decoded.count(_.result.artifact.accepted), + "diagnostic_count" -> decoded.count(!_.result.artifact.accepted) + ) + ) + ) + writeJson(root.resolve("run-errors.json"), ujson.Obj("errors" -> ujson.Arr())) + } + + private def stagingFor(item: DecodedBytecode, semantics: LuaProgramSemantics): ujson.Obj = { + val relativeName = item.relativeName + val result = item.result + val artifactId = artifactIdFor(relativeName) + val accepted = result.artifact.accepted + val prototypes = result.root.toVector.flatMap(allPrototypes) + val local = localSemantics(result) + + val exportedNamesByPrototype = exportedFunctionNamesByPrototype(semantics) + ujson.Obj( + "artifact_id" -> artifactId, + "relative_path" -> relativeName, + "decoder_status" -> (if (accepted) "accepted" else result.artifact.diagnostic.kind), + "input_kind" -> result.artifact.inputKind, + "profile" -> result.profile.map(profileJson).getOrElse(ujson.Null), + "provenance" -> "upstream-lua2cpg,bytecode-only", + "nodes" -> nodesFor(prototypes), + "edges" -> local.localFlows.map(localFlowJson), + "call_name_resolution" -> callNameResolutionRows(relativeName, result, semantics), + "unresolved_values" -> ujson.Arr(), + "upvalue_flows" -> local.upvalueFlows.map(upvalueFlowJson), + "defuse_paths" -> local.localFlows.map(defusePathJson(relativeName)), + "function_identity" -> prototypes.map(functionIdentityJson(artifactId, relativeName, exportedNamesByPrototype)), + "module_resolution" -> semantics.moduleResolutions + .filter(_.fromModulePath == relativeName) + .map(moduleResolutionJson), + "module_return_table" -> semantics.moduleReturnTables + .filter(_.modulePath == relativeName) + .map(moduleReturnTableJson), + "module_linkage" -> moduleLinkageRows(relativeName, semantics), + "call_target_candidate" -> callTargetCandidateRows(relativeName, local, semantics), + "interproc_arg_flow" -> semantics.interproceduralArgFlows + .filter(_.fromArgumentRef.startsWith(s"$relativeName:")) + .map(interproceduralArgFlowJson), + "interproc_return_flow" -> semantics.interproceduralReturnFlows + .filter(_.callerResultRef.startsWith(s"$relativeName:")) + .map(interproceduralReturnFlowJson), + "source_endpoints" -> semantics.sourceEndpoints + .filter(_.sourceRef.startsWith(s"$relativeName:")) + .map(sourceEndpointJson), + "sink_endpoints" -> semantics.sinkEndpoints.filter(_.sinkRef.startsWith(s"$relativeName:")).map(sinkEndpointJson), + "path_evidence" -> pathEvidenceRows(relativeName, semantics) + ) + } + + private def artifactSummary(item: DecodedBytecode): ujson.Obj = { + val prototypes = item.result.root.toVector.flatMap(allPrototypes) + ujson.Obj( + "artifact_id" -> artifactIdFor(item.relativeName), + "relative_path" -> item.relativeName, + "status" -> (if (item.result.artifact.accepted) "accepted" else item.result.artifact.diagnostic.kind), + "profile_id" -> item.result.profile.map(_.profileId).getOrElse("unavailable"), + "profile" -> item.result.profile.map(profileJson).getOrElse(ujson.Null), + "diagnostic_kind" -> item.result.artifact.diagnostic.kind, + "diagnostic_message" -> item.result.artifact.diagnostic.message, + "prototype_count" -> prototypes.size, + "instruction_count" -> prototypes.map(_.instructions.size).sum, + "callsite_count" -> prototypes.map(countCallsites).sum, + "flow_edge_count" -> localSemantics(item.result).localFlows.size, + "staging_graph" -> s"staging/${safeArtifactName(artifactIdFor(item.relativeName))}.json", + "path_evidence_count" -> 0 + ) + } + + private def localSemantics(result: LuaBytecodeDecodeResult): LuaPrototypeSemantics = + result.root + .map(LuaInstructionSemantics.normalize) + .getOrElse( + LuaPrototypeSemantics( + Vector.empty, + Vector.empty, + Vector.empty, + Vector.empty, + Vector.empty, + Vector.empty, + Vector.empty, + Vector.empty, + Vector.empty, + Vector.empty, + Vector.empty, + Vector.empty + ) + ) + + private def callNameResolutionRows( + relativeName: String, + result: LuaBytecodeDecodeResult, + semantics: LuaProgramSemantics + ): Vector[ujson.Obj] = { + val decodedRows = result.root.toVector.flatMap(allPrototypes).flatMap { prototype => + val callsByPrototype = + LuaInstructionSemantics.normalizePrototype(prototype).callSites.map(row => row.callsiteId -> row).toMap + resolvedCalls(prototype).map { call => + val callsite = callsByPrototype.get(call.callsiteId) + val targetValueRef = callsite + .map(_.targetValueRef) + .getOrElse( + throw new IllegalStateException(s"missing callsite semantics for ${relativeName}::${call.callsiteId}") + ) + ujson.Obj( + "resolution_id" -> s"${relativeName}:${call.callsiteId}:${call.resolvedName.getOrElse("unresolved")}", + "artifact_id" -> artifactIdFor(relativeName), + "artifact_role" -> "main", + "module_path" -> relativeName, + "prototype_id" -> call.prototypeId, + "pc" -> call.pc, + "callsite_id" -> scopedCallsite(relativeName, call.callsiteId), + "target_value_ref" -> targetValueRef, + "bytecode_pattern" -> call.resolvedName.getOrElse("unresolved-target"), + "resolved_name" -> call.resolvedName.getOrElse(""), + "name_components" -> call.resolvedName.map(_.split('.').toVector).getOrElse(Vector.empty), + "resolution_kind" -> call.resolvedName.map(_ => "global-member-chain").getOrElse("unresolved"), + "confidence" -> call.resolvedName.map(_ => "bytecode-derived").getOrElse("unresolved"), + "provenance" -> "upstream-lua2cpg,bytecode-only", + "unresolved_reason" -> call.resolvedName + .map(_ => "none") + .getOrElse("target-register-has-no-resolved-name-chain"), + "argument_value_refs" -> call.argumentRefs, + "return_value_refs" -> call.returnRefs, + "argument_constants" -> ujson.Arr(), + "direct_target_prototype_ids" -> ujson.Arr() + ) + } + } + val decodedRowsByScopedCallsite = decodedRows.map(row => row("callsite_id").str -> row).toMap + val sanitizerRows = semantics.sanitizerCalls.flatMap { row => + val (modulePath, callsiteId) = splitQualifiedRef(row.callsiteId) + if (modulePath == relativeName) { + val scopedSanitizerCallsite = toScopedStepRef(row.callsiteId) + val decodedRow = decodedRowsByScopedCallsite.getOrElse( + scopedSanitizerCallsite, + throw new IllegalStateException(s"missing decoded call row for sanitizer call: $scopedSanitizerCallsite") + ) + val argumentRefs = decodedRow("argument_value_refs").arr.map(_.str).toVector + Vector( + ujson.Obj( + "resolution_id" -> s"${row.callsiteId}:${row.sanitizerName}", + "artifact_id" -> artifactIdFor(relativeName), + "artifact_role" -> "main", + "module_path" -> modulePath, + "prototype_id" -> prototypeIdFromCallsiteId(callsiteId), + "pc" -> pcFromCallsiteId(callsiteId), + "callsite_id" -> scopedSanitizerCallsite, + "target_value_ref" -> splitQualifiedRef(row.sanitizedValueRef)._2, + "bytecode_pattern" -> s"sanitizer:${row.sanitizerName}", + "resolved_name" -> row.sanitizerName, + "name_components" -> row.sanitizerName.split('.').toVector, + "resolution_kind" -> "sanitizer-call", + "confidence" -> "bytecode-derived", + "provenance" -> row.provenance, + "unresolved_reason" -> "none", + "argument_value_refs" -> argumentRefs, + "return_value_refs" -> Vector(splitQualifiedRef(row.sanitizedValueRef)._2), + "argument_constants" -> ujson.Arr(), + "direct_target_prototype_ids" -> ujson.Arr() + ) + ) + } else Vector.empty + } + decodedRows ++ sanitizerRows + } + + private def resolvedCalls(prototype: LuaPrototype): Vector[ResolvedCall] = + prototype.instructions + .filter(instruction => instruction.opcode == LuaOpcode.Call || instruction.opcode == LuaOpcode.TailCall) + .map { instruction => + ResolvedCall( + callsiteId = s"${prototype.prototypeId}@pc${instruction.pc}", + prototypeId = prototype.prototypeId, + pc = instruction.pc, + resolvedName = callTargetNameAt(prototype, instruction.pc, instruction.a), + argumentRefs = callArgumentRefs(prototype, instruction), + returnRefs = callReturnRefs(prototype.prototypeId, instruction) + ) + } + + private final case class ResolvedCall( + callsiteId: String, + prototypeId: String, + pc: Int, + resolvedName: Option[String], + argumentRefs: Vector[String], + returnRefs: Vector[String] + ) + + private def callTargetNameAt(prototype: LuaPrototype, pc: Int, slot: Int): Option[String] = + prototype.instructions + .filter(instruction => instruction.pc < pc && instruction.a == slot) + .sortBy(_.pc) + .lastOption + .flatMap { + case instruction if instruction.opcode == LuaOpcode.GetGlobal => + constantName(prototype.constants, instruction.b) + case instruction if instruction.opcode == LuaOpcode.GetTable => + for { + baseName <- callTargetNameAt(prototype, instruction.pc, instruction.b) + field <- instruction.c.filter(_ >= 256).flatMap(value => constantName(prototype.constants, value - 256)) + } yield s"$baseName.$field" + case _ => None + } + + private def callArgumentRefs(prototype: LuaPrototype, instruction: LuaInstruction): Vector[String] = { + val slots = instruction.b match { + case 0 => ((instruction.a + 1) until prototype.maxStack).toVector + case 1 => Vector.empty + case n => ((instruction.a + 1) until (instruction.a + n)).toVector + } + slots.map(slot => s"${prototype.prototypeId}@pc${instruction.pc}:r$slot") + } + + private def callReturnRefs(prototypeId: String, instruction: LuaInstruction): Vector[String] = { + val slots = instruction.c match { + case Some(0) => Vector(instruction.a) + case Some(1) => Vector.empty + case Some(n) => (instruction.a until (instruction.a + n - 1)).toVector + case None => Vector.empty + } + slots.map(slot => s"$prototypeId@pc${instruction.pc}:r$slot") + } + + private def nodesFor(prototypes: Vector[LuaPrototype]): Vector[ujson.Obj] = + prototypes.flatMap { prototype => + prototype.instructions.map { instruction => + ujson.Obj( + "type" -> "LUA_BYTECODE_INSTRUCTION", + "prototype_id" -> prototype.prototypeId, + "pc" -> instruction.pc, + "opcode" -> instruction.opcode.mnemonic, + "callsite_id" -> s"${prototype.prototypeId}@pc${instruction.pc}", + "reads" -> readRefs(prototype.prototypeId, instruction), + "writes" -> writeRefs(prototype.prototypeId, instruction) + ) + } + } + + private def readRefs(prototypeId: String, instruction: LuaInstruction): Vector[String] = + instruction.opcode match { + case LuaOpcode.Call | LuaOpcode.TailCall => + (Vector(instruction.a) ++ callArgumentSlots(instruction)).map(slot => + s"$prototypeId@pc${instruction.pc}:r$slot" + ) + case LuaOpcode.Return => + returnSlots(instruction).map(slot => s"$prototypeId@pc${instruction.pc}:r$slot").toVector + case _ => + (Vector(instruction.b) ++ instruction.c.toVector.filter(_ < 256)).map(slot => + s"$prototypeId@pc${instruction.pc}:r$slot" + ) + } + + private def writeRefs(prototypeId: String, instruction: LuaInstruction): Vector[String] = + instruction.opcode match { + case LuaOpcode.Return => Vector.empty + case _ => Vector(s"$prototypeId@pc${instruction.pc}:r${instruction.a}") + } + + private def localFlowJson(row: LuaLocalFlow): ujson.Obj = + ujson.Obj("type" -> "REACHING_DEF", "src" -> row.sourceRef, "dst" -> row.sinkRef, "kind" -> row.edgeKind) + + private def upvalueFlowJson(row: LuaUpvalueFlow): ujson.Obj = + ujson.Obj( + "flow_id" -> s"upvalue:${row.upvalueId}:${row.captureRef}:${row.writeRef}", + "upvalue_id" -> row.upvalueId, + "capture_ref" -> row.captureRef, + "read_ref" -> row.readRef, + "write_ref" -> row.writeRef, + "provenance" -> row.provenance + ) + + private def defusePathJson(relativeName: String)(row: LuaLocalFlow): ujson.Obj = + ujson.Obj( + "path_id" -> s"$relativeName:${row.sourceRef}->${row.sinkRef}", + "source_ref" -> row.sourceRef, + "sink_ref" -> row.sinkRef, + "path_steps" -> Vector(qualify(relativeName, row.sourceRef), qualify(relativeName, row.sinkRef)), + "first_missing_edge" -> "none", + "provenance" -> row.provenance + ) + + private def functionIdentityJson( + artifactId: String, + relativeName: String, + exportedNamesByPrototype: Map[(String, String), String] + )(prototype: LuaPrototype): ujson.Obj = { + val identity = functionDisplayIdentity(exportedNamesByPrototype, relativeName, prototype.prototypeId) + ujson.Obj( + "identity_id" -> s"$relativeName:${prototype.prototypeId}", + "artifact_id" -> artifactId, + "artifact_role" -> "main", + "module_path" -> relativeName, + "prototype_id" -> prototype.prototypeId, + "display_name" -> identity.displayName, + "identity_kind" -> identity.identityKind, + "provenance" -> identity.provenance + ) + } + + private def moduleResolutionJson(row: LuaModuleResolution): ujson.Obj = + ujson.Obj( + "resolution_id" -> s"${row.fromModulePath}:${row.requireCallsiteId}:${row.requireString}", + "callsite_id" -> scopedCallsite(row.fromModulePath, row.requireCallsiteId), + "module_path" -> row.fromModulePath, + "require_string" -> row.requireString, + "resolution_status" -> row.resolutionStatus, + "target_module_path" -> row.targetModulePath.getOrElse(""), + "unresolved_reason" -> row.unresolvedReason.getOrElse("none"), + "provenance" -> row.provenance + ) + + private def moduleReturnTableJson(row: LuaModuleReturnTable): ujson.Obj = + ujson.Obj( + "return_table_id" -> s"${row.modulePath}:${row.tableRef}:${row.fieldName}", + "module_path" -> row.modulePath, + "table_ref" -> row.tableRef, + "field_name" -> row.fieldName, + "target_prototype_id" -> row.targetPrototypeId, + "provenance" -> row.provenance + ) + + private def moduleLinkageRows(relativeName: String, semantics: LuaProgramSemantics): Vector[ujson.Obj] = + semantics.moduleFieldCallTargets.filter(_.fromModulePath == relativeName).map { row => + ujson.Obj( + "linkage_id" -> s"${row.fromModulePath}:${row.callsiteId}:${row.targetModulePath}:${row.targetPrototypeId}", + "callsite_id" -> scopedCallsite(row.fromModulePath, row.callsiteId), + "module_path" -> row.fromModulePath, + "target_module_path" -> row.targetModulePath, + "target_prototype_id" -> row.targetPrototypeId, + "field_name" -> row.fieldName, + "resolution_status" -> "matched", + "provenance" -> row.provenance + ) + } + + private def callTargetCandidateRows( + relativeName: String, + local: LuaPrototypeSemantics, + semantics: LuaProgramSemantics + ): Vector[ujson.Obj] = { + val localRows = local.callTargetCandidates.map { row => + ujson.Obj( + "candidate_id" -> s"$relativeName:${row.callsiteId}:${row.targetRef}", + "callsite_id" -> scopedCallsite(relativeName, row.callsiteId), + "module_path" -> relativeName, + "target_ref" -> s"$relativeName::${row.targetRef}", + "confidence" -> row.confidence, + "resolution_status" -> "matched", + "unresolved_reason" -> "none", + "provenance" -> row.provenance + ) + } + val crossRows = semantics.crossBoundaryCallTargets.filter(_.fromModulePath == relativeName).map { row => + ujson.Obj( + "candidate_id" -> s"${row.fromModulePath}:${row.callsiteId}:${row.targetModulePath}:${row.targetPrototypeId}", + "callsite_id" -> scopedCallsite(row.fromModulePath, row.callsiteId), + "module_path" -> row.fromModulePath, + "target_module_path" -> row.targetModulePath, + "target_prototype_id" -> row.targetPrototypeId, + "target_ref" -> s"${row.targetModulePath}::${row.targetPrototypeId}", + "confidence" -> row.confidence, + "resolution_status" -> "matched", + "unresolved_reason" -> "none", + "provenance" -> row.provenance + ) + } + localRows ++ crossRows + } + + private def interproceduralArgFlowJson(row: LuaInterproceduralArgFlow): ujson.Obj = + ujson.Obj( + "flow_id" -> s"arg:${qualifiedCallsite(row.callsiteId, row.fromArgumentRef)}:${row.fromArgumentRef}:${row.toParameterRef}", + "callsite_id" -> qualifiedCallsite(row.callsiteId, row.fromArgumentRef), + "from_argument_ref" -> row.fromArgumentRef, + "argument_index" -> row.argumentIndex, + "to_parameter_ref" -> row.toParameterRef, + "target_module_path" -> row.targetModulePath, + "target_prototype_id" -> row.targetPrototypeId, + "provenance" -> row.provenance + ) + + private def interproceduralReturnFlowJson(row: LuaInterproceduralReturnFlow): ujson.Obj = + ujson.Obj( + "flow_id" -> s"return:${qualifiedCallsite(row.callsiteId, row.callerResultRef)}:${row.calleeReturnRef}:${row.callerResultRef}", + "callsite_id" -> qualifiedCallsite(row.callsiteId, row.callerResultRef), + "callee_return_ref" -> s"${row.targetModulePath}:${row.calleeReturnRef}", + "caller_result_ref" -> row.callerResultRef, + "target_module_path" -> row.targetModulePath, + "target_prototype_id" -> row.targetPrototypeId, + "provenance" -> row.provenance + ) + + private def sourceEndpointJson(row: LuaSourceEndpoint): ujson.Obj = { + val (modulePath, valueRef) = splitQualifiedRef(row.sourceRef) + ujson.Obj( + "module_path" -> modulePath, + "value_ref" -> valueRef, + "callsite_id" -> toScopedStepRef(row.callsiteId), + "trigger" -> row.trigger, + "provenance" -> row.provenance + ) + } + + private def sinkEndpointJson(row: LuaSinkEndpoint): ujson.Obj = { + val (modulePath, valueRef) = splitQualifiedRef(row.sinkRef) + ujson.Obj( + "module_path" -> modulePath, + "value_ref" -> valueRef, + "callsite_id" -> toScopedStepRef(row.callsiteId), + "trigger" -> row.trigger, + "param_idx" -> row.parameterIndex, + "provenance" -> row.provenance + ) + } + + private def pathEvidenceRows(relativeName: String, semantics: LuaProgramSemantics): Vector[ujson.Obj] = { + val exportedNamesByPrototype = exportedFunctionNamesByPrototype(semantics) + semantics.taintPaths.filter(_.sourceRef.startsWith(s"$relativeName:")).map { row => + val report = + semantics.reportClassifications.find(item => item.sourceRef == row.sourceRef && item.sinkRef == row.sinkRef) + val (sourceModule, sourceValue) = splitQualifiedRef(row.sourceRef) + val (sinkModule, sinkValue) = splitQualifiedRef(row.sinkRef) + val sourceEndpoint = semantics.sourceEndpoints + .find(_.sourceRef == row.sourceRef) + .getOrElse( + throw new IllegalStateException(s"taint path lacks source endpoint: ${row.sourceRef}->${row.sinkRef}") + ) + val sinkEndpoint = semantics.sinkEndpoints + .find(_.sinkRef == row.sinkRef) + .getOrElse(throw new IllegalStateException(s"taint path lacks sink endpoint: ${row.sourceRef}->${row.sinkRef}")) + val sourceCallsite = unqualifyCallsite(sourceEndpoint.callsiteId) + val sinkCallsite = unqualifyCallsite(sinkEndpoint.callsiteId) + val sourcePrototype = prototypeIdFromCallsiteId(sourceCallsite) + val sinkPrototype = prototypeIdFromCallsiteId(sinkCallsite) + val sourceFunctionName = displayFunctionName(exportedNamesByPrototype, sourceModule, sourcePrototype) + val sinkFunctionName = displayFunctionName(exportedNamesByPrototype, sinkModule, sinkPrototype) + ujson.Obj( + "path_id" -> s"${row.sourceRef}->${row.sinkRef}", + "baseline_report_id" -> "", + "source_module_path" -> sourceModule, + "source_function_name" -> sourceFunctionName, + "source_pc" -> pcFromCallsiteId(sourceCallsite), + "source_trigger" -> sourceEndpoint.trigger, + "sink_module_path" -> sinkModule, + "sink_function_name" -> sinkFunctionName, + "sink_pc" -> pcFromCallsiteId(sinkCallsite), + "sink_trigger" -> sinkEndpoint.trigger, + "sink_param_idx" -> sinkEndpoint.parameterIndex, + "source" -> ujson.Obj("module_path" -> sourceModule, "value_ref" -> sourceValue), + "sink" -> ujson.Obj("module_path" -> sinkModule, "value_ref" -> sinkValue), + "path_status" -> "matched", + "path_step_count" -> row.pathSteps.size, + "equivalence_level" -> "upstream-lua2cpg-native-path", + "path_steps" -> row.pathSteps.map(toScopedStepRef), + "sanitizer_hits" -> sanitizerHitsFor(row, semantics), + "classification" -> report.map(_.classification).getOrElse(row.classification), + "provenance" -> row.provenance + ) + } + } + + private def validateTaintPathEndpoints(semantics: LuaProgramSemantics): Unit = { + val sourceRefs = semantics.sourceEndpoints.map(_.sourceRef).toSet + val sinkRefs = semantics.sinkEndpoints.map(_.sinkRef).toSet + semantics.taintPaths.foreach { path => + if (!sourceRefs(path.sourceRef)) { + throw new IllegalStateException(s"taint path lacks source endpoint: ${path.sourceRef}->${path.sinkRef}") + } + if (!sinkRefs(path.sinkRef)) { + throw new IllegalStateException(s"taint path lacks sink endpoint: ${path.sourceRef}->${path.sinkRef}") + } + } + } + + private def displayFunctionName( + exportedNamesByPrototype: Map[(String, String), String], + modulePath: String, + prototypeId: String + ): String = + functionDisplayIdentity(exportedNamesByPrototype, modulePath, prototypeId).displayName + + private final case class FunctionDisplayIdentity(displayName: String, identityKind: String, provenance: String) + + private def exportedFunctionNamesByPrototype(semantics: LuaProgramSemantics): Map[(String, String), String] = + semantics.moduleReturnTables + .groupBy(item => item.modulePath -> item.targetPrototypeId) + .view + .mapValues(_.map(_.fieldName).distinct.sorted) + .collect { case (key, Vector(singleName)) => key -> singleName } + .toMap + + private def functionDisplayIdentity( + exportedNamesByPrototype: Map[(String, String), String], + modulePath: String, + prototypeId: String + ): FunctionDisplayIdentity = + exportedNamesByPrototype.get(modulePath -> prototypeId) match { + case Some(displayName) => + FunctionDisplayIdentity(displayName, "synthetic", "upstream-lua2cpg,bytecode-only,synthetic-name") + case None => + syntheticPrototypeDisplayName(prototypeId) match { + case Some(displayName) => + FunctionDisplayIdentity(displayName, "synthetic", "upstream-lua2cpg,bytecode-only,synthetic-name") + case None => + FunctionDisplayIdentity( + prototypeId, + "bytecode-prototype-id", + "upstream-lua2cpg,bytecode-only,prototype-identity" + ) + } + } + + private def syntheticPrototypeDisplayName(prototypeId: String): Option[String] = + if (prototypeId == "root") None + else { + val prefix = "root." + if (!prototypeId.startsWith(prefix)) { + throw new IllegalArgumentException( + s"unsupported Lua prototype id for synthetic function identity: $prototypeId" + ) + } + val ordinalPath = prototypeId.stripPrefix(prefix).split('.').toVector + if (ordinalPath.exists(_.forall(_.isDigit) == false)) { + throw new IllegalArgumentException(s"non-numeric Lua prototype ordinal path: $prototypeId") + } + Some(s"func_unknow_0_${ordinalPath.mkString("_")}") + } + + private def sanitizerHitsFor(path: LuaTaintPath, semantics: LuaProgramSemantics): ujson.Arr = + ujson.Arr.from( + semantics.sanitizerClassifications + .filter(row => row.sourceRef == path.sourceRef && row.sinkRef == path.sinkRef) + .map { row => + ujson.Obj( + "callsite_id" -> toScopedStepRef(row.sanitizerCallsiteId), + "sanitizer_name" -> row.sanitizerName, + "applies_to_sink" -> row.appliesToSink, + "on_dataflow_chain" -> row.onDataflowChain + ) + } + ) + + private def pathSearchProfile(semantics: LuaProgramSemantics): ujson.Obj = { + validatePerformanceAttribution(semantics) + ujson.Obj( + "status" -> "completed", + "source_endpoint_count" -> semantics.sourceEndpoints.size, + "sink_endpoint_count" -> semantics.sinkEndpoints.size, + "taint_path_count" -> semantics.taintPaths.size, + "sanitizer_classification_count" -> semantics.sanitizerClassifications.size, + "report_count" -> semantics.vulnerabilityReports.size, + "local_path_graph_module_count" -> semantics.pathSearchStats.localPathGraphModuleCount, + "local_path_graph_build_count" -> semantics.pathSearchStats.localPathGraphBuildCount, + "local_path_search_count" -> semantics.pathSearchStats.localPathSearchCount, + "distinct_local_path_query_count" -> semantics.pathSearchStats.distinctLocalPathQueryCount, + "source_sink_pair_count" -> semantics.pathSearchStats.sourceSinkPairCount, + "qualified_source_sink_pair_count" -> semantics.pathSearchStats.qualifiedSourceSinkPairCount, + "prototype_pruned_source_sink_pair_count" -> semantics.pathSearchStats.prototypePrunedSourceSinkPairCount, + "performance_attribution" -> performanceAttributionJson(semantics.performanceAttribution) + ) + } + + private val PairCounterNames = Set( + "source_reachability_check_count", + "source_reachability_accepted_count", + "prototype_unreachable_pair_count", + "source_specific_provenance_pruned_pair_count", + "parameter_position_check_count", + "parameter_position_accepted_count", + "parameter_position_pruned_count", + "path_constructor_check_count", + "path_constructor_accepted_count", + "path_constructor_pruned_count", + "bridge_argument_provenance_candidate_count", + "bridge_candidate_pc_pruned_count", + "bridge_candidate_reachability_pruned_count", + "bridge_local_path_attempt_count", + "bridge_local_path_success_count", + "local_path_search_count", + "distinct_local_path_query_count", + "local_path_cache_hit_count", + "local_path_cache_miss_count", + "local_path_graph_build_count", + "local_path_graph_cache_hit_count", + "local_path_graph_cache_miss_count", + "bridge_path_cache_hit_count", + "bridge_path_cache_miss_count", + "targeted_search_node_visit_count", + "targeted_search_edge_visit_count", + "early_candidate_short_circuit_count", + "taint_path_count", + "report_count" + ) + + private def performanceAttributionJson(attribution: LuaPerformanceAttribution): ujson.Obj = { + val aggregate = attribution.aggregateCounters + def aggregateNumber(name: String): ujson.Num = ujson.Num(aggregate(name).toDouble) + val pairProfiles = pairProfilesJson(attribution.pairProfiles) + ujson.Obj( + "schema" -> "lua-r7-performance-attribution-v1", + "unattributed_changed_family_work" -> ujson.Num(attribution.unattributedChangedFamilyWork.toDouble), + "retained_pair_profile_count" -> attribution.pairProfiles.size, + "retained_pair_profile_bytes" -> ujson + .write(pairProfiles) + .getBytes(StandardCharsets.UTF_8) + .length, + "rows" -> ujson.Obj( + "P1" -> ujson.Obj( + "candidate_count" -> ujson.Num(attribution.p1CandidateCount.toDouble), + "rejected_count" -> ujson.Num(attribution.p1RejectedCount.toDouble), + "accepted_count" -> ujson.Num(attribution.p1AcceptedCount.toDouble) + ), + "P2" -> aggregateRow( + aggregate, + "parameter_position_check_count", + "parameter_position_accepted_count", + "parameter_position_pruned_count" + ), + "P3" -> ujson.Obj( + "candidate_count" -> aggregateNumber("source_reachability_check_count"), + "accepted_count" -> aggregateNumber("source_reachability_accepted_count"), + "prototype_rejected_count" -> aggregateNumber("prototype_unreachable_pair_count"), + "provenance_rejected_count" -> aggregateNumber("source_specific_provenance_pruned_pair_count") + ), + "P4" -> ujson.Obj( + "candidate_count" -> aggregateNumber("bridge_argument_provenance_candidate_count"), + "pc_rejected_count" -> aggregateNumber("bridge_candidate_pc_pruned_count"), + "reachability_rejected_count" -> aggregateNumber("bridge_candidate_reachability_pruned_count"), + "continued_count" -> ujson.Num( + (aggregate("bridge_argument_provenance_candidate_count") - aggregate( + "bridge_candidate_pc_pruned_count" + ) - aggregate("bridge_candidate_reachability_pruned_count")).toDouble + ), + "path_constructor_candidate_count" -> aggregateNumber("path_constructor_check_count"), + "path_constructor_accepted_count" -> aggregateNumber("path_constructor_accepted_count"), + "path_constructor_rejected_count" -> aggregateNumber("path_constructor_pruned_count") + ), + "P5" -> ujson.Obj( + "node_visit_count" -> aggregateNumber("targeted_search_node_visit_count"), + "edge_visit_count" -> aggregateNumber("targeted_search_edge_visit_count") + ), + "P6" -> ujson.Obj( + "local_path_cache_hit_count" -> aggregateNumber("local_path_cache_hit_count"), + "local_path_cache_miss_count" -> aggregateNumber("local_path_cache_miss_count"), + "local_path_graph_cache_hit_count" -> aggregateNumber("local_path_graph_cache_hit_count"), + "local_path_graph_cache_miss_count" -> aggregateNumber("local_path_graph_cache_miss_count"), + "bridge_path_cache_hit_count" -> aggregateNumber("bridge_path_cache_hit_count"), + "bridge_path_cache_miss_count" -> aggregateNumber("bridge_path_cache_miss_count") + ), + "P7" -> ujson.Obj("status" -> "not-invoked-no-reuse", "invocation_count" -> 0, "reuse_count" -> 0), + "early_short_circuit" -> ujson.Obj( + "count" -> aggregateNumber("early_candidate_short_circuit_count"), + "pc_rejected_count" -> aggregateNumber("bridge_candidate_pc_pruned_count"), + "reachability_rejected_count" -> aggregateNumber("bridge_candidate_reachability_pruned_count") + ) + ), + "pair_profiles" -> pairProfiles + ) + } + + private def pairProfilesJson(rows: Vector[LuaPairPerformanceProfile]): ujson.Arr = + ujson.Arr.from(rows.map { row => + ujson.Obj.from( + Vector( + "pair_id" -> ujson.Str(row.pairId), + "source_ref" -> ujson.Str(row.sourceRef), + "sink_ref" -> ujson.Str(row.sinkRef), + "source_callsite_id" -> ujson.Str(row.sourceCallsiteId), + "sink_callsite_id" -> ujson.Str(row.sinkCallsiteId), + "source_trigger" -> ujson.Str(row.sourceTrigger), + "sink_trigger" -> ujson.Str(row.sinkTrigger) + ) ++ row.counters.toVector.sortBy(_._1).map { case (name, value) => name -> ujson.Num(value.toDouble) } + ) + }) + + private def aggregateRow(count: Map[String, Long], total: String, accepted: String, rejected: String): ujson.Obj = + ujson.Obj( + "candidate_count" -> ujson.Num(count(total).toDouble), + "accepted_count" -> ujson.Num(count(accepted).toDouble), + "rejected_count" -> ujson.Num(count(rejected).toDouble) + ) + + private def validatePerformanceAttribution(semantics: LuaProgramSemantics): Unit = { + val attribution = semantics.performanceAttribution + def requireCount(condition: Boolean, message: String): Unit = + if (!condition) throw new IllegalStateException(s"invalid Lua performance attribution: $message") + + requireCount( + attribution.p1CandidateCount == attribution.p1RejectedCount + attribution.p1AcceptedCount, + "P1 candidate count does not partition into accepted and rejected counts" + ) + requireCount( + attribution.unattributedChangedFamilyWork == 0L, + s"unattributed changed-family work is ${attribution.unattributedChangedFamilyWork}" + ) + requireCount( + attribution.aggregateCounters.keySet == PairCounterNames, + "aggregate counter keys do not exactly match the required schema" + ) + val continuedBridgeCandidates = + attribution.aggregateCounters("bridge_argument_provenance_candidate_count") - + attribution.aggregateCounters("early_candidate_short_circuit_count") + val requiresPairProfiles = attribution.aggregateCounters("local_path_search_count") > 0L || + attribution.aggregateCounters("taint_path_count") > 0L || continuedBridgeCandidates > 0L + requireCount( + !requiresPairProfiles || attribution.pairProfiles.nonEmpty, + "pair profiles are empty despite retained pair work" + ) + validateCounterPartitions(attribution.aggregateCounters, "aggregate") + requireCount( + attribution.pairProfiles.map(_.pairId).distinct.size == attribution.pairProfiles.size, + "pair identities are not unique" + ) + requireCount( + attribution.pairProfiles.size <= semantics.pathSearchStats.localPathSearchCount + semantics.taintPaths.size, + "retained pair profile count exceeds local-search plus taint-path bound" + ) + val retainedPairProfileBytes = ujson + .write(pairProfilesJson(attribution.pairProfiles)) + .getBytes(StandardCharsets.UTF_8) + .length + requireCount( + attribution.pairProfiles.isEmpty || + retainedPairProfileBytes <= attribution.pairProfiles.size.toLong * MaxRetainedPairProfileBytes, + s"retained pair profile payload exceeds $MaxRetainedPairProfileBytes bytes per row" + ) + requireCount( + attribution.aggregateCounters("source_reachability_check_count") == semantics.pathSearchStats.sourceSinkPairCount, + "aggregate source reachability count does not match legacy source-sink pair count" + ) + requireCount( + attribution.aggregateCounters( + "parameter_position_accepted_count" + ) == semantics.pathSearchStats.qualifiedSourceSinkPairCount, + "aggregate parameter-position accepted count does not match legacy qualified pair count" + ) + requireCount( + attribution.aggregateCounters("local_path_search_count") == semantics.pathSearchStats.localPathSearchCount, + "aggregate local path search count does not match legacy count" + ) + requireCount( + attribution.aggregateCounters( + "distinct_local_path_query_count" + ) == semantics.pathSearchStats.distinctLocalPathQueryCount, + "aggregate distinct local path query count does not match legacy count" + ) + requireCount( + attribution.aggregateCounters( + "local_path_graph_build_count" + ) == semantics.pathSearchStats.localPathGraphBuildCount, + "aggregate local path graph build count does not match legacy count" + ) + attribution.pairProfiles.foreach { row => + val count = row.counters + requireCount( + count.keySet == PairCounterNames, + s"counter keys do not exactly match the required schema for pair ${row.pairId}" + ) + requireIdentity(row) + requireCount( + count("local_path_search_count") > 0L || count("taint_path_count") > 0L, + s"retained pair has neither local-search nor taint-path work for pair ${row.pairId}" + ) + requireCount(count.values.forall(_ >= 0L), s"negative counter for pair ${row.pairId}") + requireCount( + count("source_reachability_check_count") == count("source_reachability_accepted_count") + + count("prototype_unreachable_pair_count") + count("source_specific_provenance_pruned_pair_count"), + s"source reachability partition mismatch for pair ${row.pairId}" + ) + requireCount( + count("parameter_position_check_count") == count("parameter_position_accepted_count") + + count("parameter_position_pruned_count"), + s"parameter position partition mismatch for pair ${row.pairId}" + ) + requireCount( + count("path_constructor_check_count") == count("path_constructor_accepted_count") + + count("path_constructor_pruned_count"), + s"path constructor partition mismatch for pair ${row.pairId}" + ) + requireCount( + count("bridge_argument_provenance_candidate_count") >= count("bridge_candidate_pc_pruned_count") + + count("bridge_candidate_reachability_pruned_count"), + s"bridge candidate partition mismatch for pair ${row.pairId}" + ) + requireCount( + count("early_candidate_short_circuit_count") == count("bridge_candidate_pc_pruned_count") + + count("bridge_candidate_reachability_pruned_count"), + s"early short-circuit partition mismatch for pair ${row.pairId}" + ) + requireCount( + count("local_path_search_count") == count("local_path_cache_hit_count") + + count("local_path_cache_miss_count"), + s"local path cache partition mismatch for pair ${row.pairId}" + ) + requireCount( + count("distinct_local_path_query_count") == count("local_path_cache_miss_count"), + s"distinct local path query mismatch for pair ${row.pairId}" + ) + requireCount( + count("local_path_graph_build_count") == count("local_path_graph_cache_miss_count"), + s"local path graph build mismatch for pair ${row.pairId}" + ) + requireCount( + count("taint_path_count") == semantics.taintPaths.count(path => + path.sourceRef == row.sourceRef && path.sinkRef == row.sinkRef + ), + s"path reconciliation mismatch for pair ${row.pairId}" + ) + requireCount( + count("report_count") == semantics.reportClassifications.count(report => + report.sourceRef == row.sourceRef && report.sinkRef == row.sinkRef + ), + s"report reconciliation mismatch for pair ${row.pairId}" + ) + } + } + + private def validateCounterPartitions(count: Map[String, Long], identity: String): Unit = { + def requireCount(condition: Boolean, message: String): Unit = + if (!condition) throw new IllegalStateException(s"invalid Lua performance attribution: $message") + requireCount(count.values.forall(_ >= 0L), s"negative counter for $identity") + requireCount( + count("source_reachability_check_count") == count("source_reachability_accepted_count") + count( + "prototype_unreachable_pair_count" + ) + count("source_specific_provenance_pruned_pair_count"), + s"source reachability partition mismatch for $identity" + ) + requireCount( + count("parameter_position_check_count") == count("parameter_position_accepted_count") + count( + "parameter_position_pruned_count" + ), + s"parameter position partition mismatch for $identity" + ) + requireCount( + count("path_constructor_check_count") == count("path_constructor_accepted_count") + count( + "path_constructor_pruned_count" + ), + s"path constructor partition mismatch for $identity" + ) + requireCount( + count("bridge_argument_provenance_candidate_count") >= count("bridge_candidate_pc_pruned_count") + count( + "bridge_candidate_reachability_pruned_count" + ), + s"bridge candidate partition mismatch for $identity" + ) + requireCount( + count("early_candidate_short_circuit_count") == count("bridge_candidate_pc_pruned_count") + count( + "bridge_candidate_reachability_pruned_count" + ), + s"early short-circuit partition mismatch for $identity" + ) + requireCount( + count("local_path_search_count") == count("local_path_cache_hit_count") + count("local_path_cache_miss_count"), + s"local path cache partition mismatch for $identity" + ) + requireCount( + count("distinct_local_path_query_count") == count("local_path_cache_miss_count"), + s"distinct local path query mismatch for $identity" + ) + requireCount( + count("local_path_graph_build_count") == count("local_path_graph_cache_miss_count"), + s"local path graph build mismatch for $identity" + ) + } + + private def requireIdentity(row: LuaPairPerformanceProfile): Unit = { + def invalid(message: String): Nothing = + throw new IllegalStateException(s"invalid Lua performance attribution: $message for pair ${row.pairId}") + val ValueRef = raw"(.+):([^:]+)@pc([0-9]+):r([0-9]+)".r + val Callsite = raw"(.+)::([^:]+)@pc([0-9]+)".r + def validate(ref: String, callsite: String, side: String): Unit = (ref, callsite) match { + case (ValueRef(refModule, refPrototype, refPc, _), Callsite(callModule, callPrototype, callPc)) + if refModule == callModule && refPrototype == callPrototype && refPc == callPc => + case _ => invalid(s"malformed or mismatched $side identity") + } + if (row.sourceTrigger.isEmpty || row.sinkTrigger.isEmpty) invalid("empty trigger") + validate(row.sourceRef, row.sourceCallsiteId, "source") + validate(row.sinkRef, row.sinkCallsiteId, "sink") + } + + private def profileJson(profile: LuaBytecodeProfile): ujson.Obj = + ujson.Obj( + "lua_version" -> profile.luaVersion, + "bytecode_version" -> profile.bytecodeVersion, + "format" -> profile.format, + "endianness" -> profile.endianness, + "int_size" -> profile.intSize, + "size_t_size" -> profile.sizeTSize, + "instruction_size" -> profile.instructionSize, + "lua_number_size" -> profile.luaNumberSize, + "number_mode" -> profile.numberMode, + "profile_id" -> profile.profileId + ) + + private def allPrototypes(prototype: LuaPrototype): Vector[LuaPrototype] = + prototype +: prototype.nested.flatMap(allPrototypes) + + private def countCallsites(prototype: LuaPrototype): Int = + prototype.instructions.count(instruction => + instruction.opcode == LuaOpcode.Call || instruction.opcode == LuaOpcode.TailCall + ) + + private def callArgumentSlots(instruction: LuaInstruction): Vector[Int] = + instruction.b match { + case 0 => Vector(instruction.a + 1) + case 1 => Vector.empty + case n => ((instruction.a + 1) until (instruction.a + n)).toVector + } + + private def returnSlots(instruction: LuaInstruction): Seq[Int] = + instruction.b match { + case 0 => Seq.empty + case 1 => Seq.empty + case n => instruction.a until (instruction.a + n - 1) + } + + private def constantName(constants: Vector[LuaConstant], index: Int): Option[String] = + constants.collectFirst { case LuaConstant(`index`, "string", LuaConstantValue.StringValue(value)) => + value + } + + private def toScopedStepRef(valueRef: String): String = { + val (modulePath, localRef) = splitQualifiedRef(valueRef) + s"$modulePath::$localRef" + } + + private def qualify(modulePath: String, ref: String): String = s"$modulePath:$ref" + + private def qualifiedCallsite(callsiteId: String, valueRef: String): String = + s"${splitQualifiedRef(valueRef)._1}::$callsiteId" + + private def scopedCallsite(modulePath: String, callsiteId: String): String = + s"$modulePath::$callsiteId" + + private def unqualifyCallsite(ref: String): String = + ref.split(':').lastOption.getOrElse(ref) + + private def splitQualifiedRef(ref: String): (String, String) = { + val splitAt = Vector(".luac:") + .flatMap(marker => { + val index = ref.indexOf(marker) + if (index >= 0) Some(index + marker.length - 1) else None + }) + .headOption + .getOrElse(throw new IllegalArgumentException(s"Lua qualified ref is missing module path: $ref")) + (ref.substring(0, splitAt), ref.substring(splitAt + 1)) + } + + private def prototypeIdFromValueRef(ref: String): String = + ref.split("@pc", 2).headOption.getOrElse(ref) + + private def pcFromValueRef(ref: String): Int = + ref.split("@pc", 2).lift(1).flatMap(_.split(":r", 2).headOption).flatMap(_.toIntOption).getOrElse(0) + + private def prototypeIdFromCallsiteId(ref: String): String = + ref.split("@pc", 2).headOption.getOrElse(ref) + + private def pcFromCallsiteId(ref: String): Int = + ref.split("@pc", 2).lift(1).flatMap(_.toIntOption).getOrElse(0) + + private def artifactIdFor(relativeName: String): String = s"sha256:${relativeName}" + + private def safeArtifactName(artifactId: String): String = + artifactId.map { + case character if character.isLetterOrDigit => character + case _ => '_' + } + + private def writeJson(path: Path, value: ujson.Value): Unit = { + Files.createDirectories(path.getParent) + Files.write(path, (ujson.write(value, indent = 2) + "\n").getBytes(StandardCharsets.UTF_8)) + } +} diff --git a/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/passes/LuaBytecodeModelPass.scala b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/passes/LuaBytecodeModelPass.scala new file mode 100644 index 000000000000..072b83806c99 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/passes/LuaBytecodeModelPass.scala @@ -0,0 +1,582 @@ +package io.joern.lua2cpg.passes + +import io.joern.lua2cpg.Config +import io.joern.lua2cpg.bytecode.* +import io.joern.x2cpg.{Ast, Defines, SourceFiles, ValidationMode} +import io.shiftleft.codepropertygraph.generated.nodes.* +import io.shiftleft.codepropertygraph.generated.{Cpg, DispatchTypes, EdgeTypes, EvaluationStrategies, NodeTypes} +import io.shiftleft.passes.CpgPass +import io.shiftleft.semanticcpg.utils.FileUtil.* + +import java.nio.file.{Files, Paths} +import scala.collection.mutable + +class LuaBytecodeModelPass( + cpg: Cpg, + config: Config, + decodedInputs: Option[Vector[LuaBytecodeModelPass.DecodedBytecode]] = None +) extends CpgPass(cpg) { + + private given ValidationMode = ValidationMode.Disabled + + private final case class PrototypeAst(ast: Ast, reachingDefEdges: Vector[(NewNode, NewNode, String)]) + override def run(diffGraph: DiffGraphBuilder): Unit = { + val decoded = decodedInputs.getOrElse(LuaBytecodeModelPass.decodeInputs(config)) + val programSemantics = LuaProgramSemantics.normalize(decoded.map(item => item.relativeName -> item.result)) + + decoded.foreach { decodedItem => + val relativeName = decodedItem.relativeName + val result = decodedItem.result + + diffGraph.addNode(NewFile().name(relativeName).order(decodedItem.order)) + addArtifact(result, relativeName, diffGraph) + if (canEmitSuccessFacts(result)) { + result.root.foreach(root => addPrototypeTree(root, relativeName, programSemantics, diffGraph)) + } + addDiagnostic(result, relativeName, diffGraph) + if (isStrippedMetadata(result)) { + addMetadataUnavailableDiagnostic(relativeName, diffGraph) + } + } + } + + private def canEmitSuccessFacts(result: LuaBytecodeDecodeResult): Boolean = + result.artifact.accepted && result.artifact.diagnostic.successFactsAllowed + + private def addArtifact(result: LuaBytecodeDecodeResult, relativeName: String, diffGraph: DiffGraphBuilder): Unit = { + diffGraph.addNode( + NewTypeDecl() + .name("lua-bytecode-artifact") + .fullName(s"lua:$relativeName") + .code(artifactCode(result)) + .filename(relativeName) + .isExternal(false) + .astParentType(NodeTypes.FILE) + .astParentFullName(relativeName) + ) + } + + private def addPrototypeTree( + prototype: LuaPrototype, + relativeName: String, + programSemantics: LuaProgramSemantics, + diffGraph: DiffGraphBuilder + ): Unit = { + val prototypeTypeDecl = NewTypeDecl() + .name("lua-bytecode-prototype") + .fullName(prototypeTypeFullName(relativeName, prototype)) + .code(prototypeCode(prototype)) + .filename(relativeName) + .isExternal(false) + .astParentType(NodeTypes.FILE) + .astParentFullName(relativeName) + + diffGraph.addNode(prototypeTypeDecl) + val methodAst = prototypeMethodAst(prototype, relativeName, programSemantics) + Ast.storeInDiffGraph(methodAst.ast, diffGraph) + methodAst.reachingDefEdges.foreach { case (source, sink, variable) => + diffGraph.addEdge(source, sink, EdgeTypes.REACHING_DEF, variable) + } + prototype.nested.foreach(child => addPrototypeTree(child, relativeName, programSemantics, diffGraph)) + } + + private def prototypeMethodAst( + prototype: LuaPrototype, + relativeName: String, + programSemantics: LuaProgramSemantics + ): PrototypeAst = { + val fullName = prototypeMethodFullName(relativeName, prototype) + val semantics = LuaInstructionSemantics.normalizePrototype(prototype) + val method = NewMethod() + .name(prototype.prototypeId) + .code(prototypeCode(prototype)) + .fullName(fullName) + .filename(relativeName) + .signature(prototypeSignature(prototype)) + .isExternal(false) + .astParentType(NodeTypes.TYPE_DECL) + .astParentFullName(prototypeTypeFullName(relativeName, prototype)) + + val parameters = (0 until prototype.numParams).map { index => + Ast( + NewMethodParameterIn() + .name(s"r$index") + .code(s"${prototype.prototypeId}:r$index") + .index(index + 1) + .order(index + 1) + .isVariadic(false) + .evaluationStrategy(EvaluationStrategies.BY_VALUE) + .typeFullName(Defines.Any) + ) + } + val semanticNodes = semanticValueNodes(prototype, semantics) ++ + semanticCallNodes(prototype, semantics) ++ + programSemanticCallNodes(prototype, relativeName, programSemantics) + val block = Ast(NewBlock().code(prototype.prototypeId).typeFullName(Defines.Any)) + .withChildren(prototype.constants.map(constant => Ast(literalNode(prototype, constant)))) + .withChildren(prototype.instructions.map(instruction => Ast(instructionNode(prototype, instruction)))) + .withChildren(semanticNodes.map(Ast(_))) + val methodReturn = Ast( + NewMethodReturn() + .code("RET") + .order(prototype.numParams + 2) + .evaluationStrategy(EvaluationStrategies.BY_VALUE) + .typeFullName(Defines.Any) + ) + + val ast = Ast(method) + .withChildren(parameters) + .withChild(block) + .withChild(methodReturn) + PrototypeAst(ast, reachingDefEdges(ast, semantics)) + } + + private def literalNode(prototype: LuaPrototype, constant: LuaConstant): NewLiteral = { + val text = constantValueText(constant.value) + NewLiteral() + .code(text) + .typeFullName(s"lua.${constant.luaType}") + .order(constant.index + 1) + .argumentIndex(constant.index + 1) + .lineNumber((constant.index + 1).toInt) + .columnNumber(0) + } + + private def instructionNode(prototype: LuaPrototype, instruction: LuaInstruction): NewCall = + NewCall() + .name(s"lua.bytecode.${instruction.opcode.mnemonic}") + .code(s"${prototype.prototypeId}@pc${instruction.pc}:${instruction.opcode.mnemonic}") + .methodFullName(s"lua.bytecode.${instruction.opcode.mnemonic}") + .dispatchType(DispatchTypes.STATIC_DISPATCH) + .typeFullName(Defines.Any) + .order(instruction.pc + 1) + .argumentIndex(instruction.pc + 1) + .lineNumber(instruction.pc + 1) + .columnNumber(0) + + private def semanticValueNodes(prototype: LuaPrototype, semantics: LuaPrototypeSemantics): Vector[NewIdentifier] = + semantics.registerEvents + .filter(_.prototypeId == prototype.prototypeId) + .map(_.valueRef) + .distinct + .sorted + .zipWithIndex + .map { case (ref, index) => + NewIdentifier() + .name(ref) + .code(ref) + .typeFullName(Defines.Any) + .order(10_000 + index) + .argumentIndex(10_000 + index) + .lineNumber(10_000 + index) + .columnNumber(0) + } + + private def semanticCallNodes(prototype: LuaPrototype, semantics: LuaPrototypeSemantics): Vector[NewCall] = { + val candidateNodes = semantics.callTargetCandidates + .filter(_.callsiteId.startsWith(s"${prototype.prototypeId}@pc")) + .sortBy(candidate => (candidate.callsiteId, candidate.targetRef)) + .zipWithIndex + .map { case (candidate, index) => + semanticCallNode( + name = "lua.calltarget.candidate", + code = s"${candidate.callsiteId} -> ${candidate.targetRef}", + order = 20_000 + index + ) + } + val unresolvedNodes = semantics.unresolvedCalls + .filter(_.callsiteId.startsWith(s"${prototype.prototypeId}@pc")) + .sortBy(_.callsiteId) + .zipWithIndex + .map { case (call, index) => + semanticCallNode( + name = "lua.calltarget.unresolved", + code = s"${call.callsiteId} unresolved=${call.unresolvedReason}", + order = 30_000 + index + ) + } + val boundaryNodes = semantics.negativeExpectations + .filter(row => + row.sourceRef.startsWith(s"${prototype.prototypeId}@") || row.sourceRef.startsWith(s"${prototype.prototypeId}.") + ) + .sortBy(_.negativeId) + .zipWithIndex + .map { case (row, index) => + semanticCallNode(name = "lua.semantic.boundary", code = row.negativeId, order = 40_000 + index) + } + candidateNodes ++ unresolvedNodes ++ boundaryNodes + } + + private def programSemanticCallNodes( + prototype: LuaPrototype, + relativeName: String, + semantics: LuaProgramSemantics + ): Vector[NewCall] = { + val prefix = s"$relativeName:${prototype.prototypeId}@pc" + val rootMethod = prototype.prototypeId == "root" + val rootMarkers = if (rootMethod) { + val moduleResolutions = semantics.moduleResolutions + .filter(_.fromModulePath == relativeName) + .sortBy(_.requireCallsiteId) + .zipWithIndex + .map { case (resolution, index) => + val target = resolution match { + case LuaModuleResolution(_, _, "matched", _, Some(targetModulePath), None, _) => targetModulePath + case LuaModuleResolution(_, _, _, _, None, Some(unresolvedReason), _) => unresolvedReason + case _ => + throw new IllegalStateException( + s"inconsistent Lua module resolution state at ${resolution.fromModulePath}:${resolution.requireCallsiteId}" + ) + } + semanticCallNode( + name = "lua.module.resolution", + code = + s"${resolution.fromModulePath} require ${resolution.requireString} -> ${resolution.resolutionStatus}:$target", + order = 50_000 + index + ) + } + val returnTables = semantics.moduleReturnTables + .filter(_.modulePath == relativeName) + .sortBy(row => (row.fieldName, row.targetPrototypeId)) + .zipWithIndex + .map { case (row, index) => + semanticCallNode( + name = "lua.module.return_table", + code = s"${row.modulePath}::${row.fieldName} -> ${row.targetPrototypeId}", + order = 51_000 + index + ) + } + val boundaries = semantics.boundaries + .filter(_.boundaryId.startsWith(relativeName)) + .sortBy(_.boundaryId) + .zipWithIndex + .map { case (boundary, index) => + semanticCallNode( + name = "lua.e4.boundary", + code = s"${boundary.boundaryId} reason=${boundary.reason}", + order = 52_000 + index + ) + } + val e5Boundaries = semantics.e5Boundaries + .filter(_.boundaryId.startsWith(relativeName)) + .sortBy(_.boundaryId) + .zipWithIndex + .map { case (boundary, index) => + semanticCallNode( + name = "lua.e5.boundary", + code = s"${boundary.boundaryId} reason=${boundary.reason}", + order = 52_500 + index + ) + } + moduleResolutions ++ returnTables ++ boundaries ++ e5Boundaries + } else Vector.empty + + val fieldCalls = semantics.moduleFieldCallTargets + .filter(row => s"${row.fromModulePath}:${row.callsiteId}".startsWith(prefix)) + .sortBy(_.callsiteId) + .zipWithIndex + .map { case (row, index) => + semanticCallNode( + name = "lua.module.field_call_target", + code = s"${row.fromModulePath}:${row.callsiteId} -> ${row.targetModulePath}::${row.targetPrototypeId}", + order = 53_000 + index + ) + } + val argFlows = semantics.interproceduralArgFlows + .filter(row => + row.callsiteId.startsWith(s"${prototype.prototypeId}@pc") && row.fromArgumentRef.startsWith(relativeName) + ) + .sortBy(row => (row.callsiteId, row.fromArgumentRef)) + .zipWithIndex + .map { case (row, index) => + semanticCallNode( + name = "lua.interproc.arg_flow", + code = s"${row.fromArgumentRef} -> ${row.toParameterRef}", + order = 54_000 + index + ) + } + val returnFlows = semantics.interproceduralReturnFlows + .filter(row => + row.callsiteId.startsWith(s"${prototype.prototypeId}@pc") && row.callerResultRef.startsWith(relativeName) + ) + .sortBy(row => (row.callsiteId, row.callerResultRef)) + .zipWithIndex + .map { case (row, index) => + semanticCallNode( + name = "lua.interproc.return_flow", + code = s"${row.targetModulePath}::${row.calleeReturnRef} -> ${row.callerResultRef}", + order = 55_000 + index + ) + } + val crossTargets = semantics.crossBoundaryCallTargets + .filter(row => + row.fromModulePath == relativeName && s"${row.fromModulePath}:${row.callsiteId}".startsWith(prefix) + ) + .sortBy(row => (row.callsiteId, row.targetModulePath, row.targetPrototypeId)) + .zipWithIndex + .map { case (row, index) => + semanticCallNode( + name = "lua.calltarget.cross_boundary", + code = s"$relativeName:${row.callsiteId} -> ${row.targetModulePath}::${row.targetPrototypeId}", + order = 56_000 + index + ) + } + val taintPaths = semantics.taintPaths + .filter(row => row.sourceRef.startsWith(s"$relativeName:${prototype.prototypeId}@")) + .sortBy(row => (row.sourceRef, row.sinkRef)) + .zipWithIndex + .map { case (row, index) => + semanticCallNode( + name = "lua.taint.path", + code = s"${row.sourceRef} -> ${row.sinkRef} via ${row.pathSteps.mkString(";")}", + order = 57_000 + index + ) + } + val ruleMatches = semantics.ruleMatches + .filter(row => row.callsiteId.startsWith(prefix)) + .sortBy(row => (row.ruleKind, row.callsiteId, row.matchedName)) + .zipWithIndex + .map { case (row, index) => + semanticCallNode( + name = "lua.rule.match", + code = s"${row.callsiteId} ${row.trigger} -> ${row.matchedName}", + order = 58_000 + index + ) + } + val sourceEndpoints = semantics.sourceEndpoints + .filter(row => row.sourceRef.startsWith(s"$relativeName:${prototype.prototypeId}@")) + .sortBy(row => (row.sourceRef, row.trigger)) + .zipWithIndex + .map { case (row, index) => + semanticCallNode( + name = "lua.source.endpoint", + code = s"${row.sourceRef} via ${row.trigger}", + order = 59_000 + index + ) + } + val sinkEndpoints = semantics.sinkEndpoints + .filter(row => row.sinkRef.startsWith(s"$relativeName:${prototype.prototypeId}@")) + .sortBy(row => (row.sinkRef, row.trigger)) + .zipWithIndex + .map { case (row, index) => + semanticCallNode( + name = "lua.sink.endpoint", + code = s"${row.sinkRef} via ${row.trigger} param=${row.parameterIndex}", + order = 60_000 + index + ) + } + val sanitizerCalls = semantics.sanitizerCalls + .filter(row => row.sanitizedValueRef.startsWith(s"$relativeName:${prototype.prototypeId}@")) + .sortBy(row => (row.callsiteId, row.sanitizerName)) + .zipWithIndex + .map { case (row, index) => + semanticCallNode( + name = "lua.sanitizer.call", + code = s"${row.callsiteId} ${row.sanitizerName} -> ${row.sanitizedValueRef}", + order = 61_000 + index + ) + } + val sanitizerClassifications = semantics.sanitizerClassifications + .filter(row => row.sourceRef.startsWith(s"$relativeName:${prototype.prototypeId}@")) + .sortBy(row => (row.sourceRef, row.sinkRef, row.sanitizerCallsiteId)) + .zipWithIndex + .map { case (row, index) => + semanticCallNode( + name = "lua.sanitizer.classification", + code = + s"${row.sourceRef} -> ${row.sinkRef} classification=${row.classification} sanitizer=${row.sanitizerName}", + order = 62_000 + index + ) + } + val reportClassifications = semantics.reportClassifications + .filter(row => row.sourceRef.startsWith(s"$relativeName:${prototype.prototypeId}@")) + .sortBy(row => (row.sourceRef, row.sinkRef)) + .zipWithIndex + .map { case (row, index) => + semanticCallNode( + name = "lua.report.classification", + code = s"${row.sourceRef} -> ${row.sinkRef} classification=${row.classification} reason=${row.reason}", + order = 63_000 + index + ) + } + val vulnerabilityReports = semantics.vulnerabilityReports + .filter(row => row.sourceRef.startsWith(s"$relativeName:${prototype.prototypeId}@")) + .sortBy(row => (row.sourceRef, row.sinkRef)) + .zipWithIndex + .map { case (row, index) => + semanticCallNode( + name = "lua.report.vulnerability", + code = + s"${row.sourceRef} -> ${row.sinkRef} status=${row.pathStatus} classification=${row.classification} path=${row.pathSteps + .mkString(";")}", + order = 64_000 + index + ) + } + + rootMarkers ++ fieldCalls ++ argFlows ++ returnFlows ++ crossTargets ++ taintPaths ++ ruleMatches ++ + sourceEndpoints ++ sinkEndpoints ++ sanitizerCalls ++ sanitizerClassifications ++ reportClassifications ++ + vulnerabilityReports + } + + private def semanticCallNode(name: String, code: String, order: Int): NewCall = + NewCall() + .name(name) + .code(code) + .methodFullName(name) + .dispatchType(DispatchTypes.STATIC_DISPATCH) + .typeFullName(Defines.Any) + .order(order) + .argumentIndex(order) + .lineNumber(order) + .columnNumber(0) + + private def reachingDefEdges(ast: Ast, semantics: LuaPrototypeSemantics): Vector[(NewNode, NewNode, String)] = { + val nodesByCode = ast.nodes.collect { + case node: NewIdentifier => node.code -> node + case node: NewMethodParameterIn => node.code -> node + }.toMap + val edges = mutable.LinkedHashSet.empty[(NewNode, NewNode, String)] + semantics.localFlows.foreach { flow => + addEdge(nodesByCode, edges, flow.sourceRef, flow.sinkRef, flow.sourceRef) + } + semantics.tableFieldFlows.foreach { flow => + addEdge(nodesByCode, edges, flow.writeRef, flow.readRef, s"table:${flow.tableRef}:${flow.keyRef}") + } + semantics.globalFlows.foreach { flow => + addEdge(nodesByCode, edges, flow.valueRef, flow.readRef, s"global:${flow.globalName}") + } + semantics.upvalueFlows.foreach { flow => + addEdge(nodesByCode, edges, flow.captureRef, flow.writeRef, s"upvalue:${flow.upvalueId}") + } + edges.toVector + } + + private def addEdge( + nodesByCode: Map[String, NewNode], + edges: mutable.LinkedHashSet[(NewNode, NewNode, String)], + sourceRef: String, + sinkRef: String, + variable: String + ): Unit = + for { + source <- nodesByCode.get(sourceRef) + sink <- nodesByCode.get(sinkRef) + } edges += ((source, sink, variable)) + + private def addDiagnostic( + result: LuaBytecodeDecodeResult, + relativeName: String, + diffGraph: DiffGraphBuilder + ): Unit = { + val diagnostic = result.artifact.diagnostic + diffGraph.addNode( + NewTypeDecl() + .name("lua-bytecode-diagnostic") + .fullName(s"lua:$relativeName:diagnostic:${diagnostic.kind}") + .code(diagnosticCode(result, diagnostic)) + .filename(relativeName) + .isExternal(false) + .astParentType(NodeTypes.FILE) + .astParentFullName(relativeName) + ) + } + + private def addMetadataUnavailableDiagnostic(relativeName: String, diffGraph: DiffGraphBuilder): Unit = { + diffGraph.addNode( + NewTypeDecl() + .name("lua-bytecode-diagnostic") + .fullName(s"lua:$relativeName:diagnostic:metadata-unavailable") + .code("kind=metadata-unavailable severity=info success_facts_allowed=true") + .filename(relativeName) + .isExternal(false) + .astParentType(NodeTypes.FILE) + .astParentFullName(relativeName) + ) + } + + private def isStrippedMetadata(result: LuaBytecodeDecodeResult): Boolean = { + result.artifact.accepted && result.root.exists { root => + allPrototypes(root).exists(prototype => + prototype.sourceName.isEmpty && + prototype.lineNumbers.isEmpty && + prototype.locals.isEmpty && + prototype.upvalueNames.isEmpty + ) + } + } + + private def allPrototypes(prototype: LuaPrototype): Vector[LuaPrototype] = + prototype +: prototype.nested.flatMap(allPrototypes) + + private def artifactCode(result: LuaBytecodeDecodeResult): String = { + val artifact = result.artifact + Seq( + s"input_kind=${artifact.inputKind}", + s"accepted=${artifact.accepted}", + s"diagnostic=${artifact.diagnostic.kind}", + artifact.profileId.map(profile => s"profile=$profile").getOrElse("profile=unavailable") + ).mkString(" ") + } + + private def prototypeCode(prototype: LuaPrototype): String = { + Seq( + s"prototype=${prototype.prototypeId}", + s"parent=${prototype.parentPrototypeId.getOrElse("none")}", + s"params=${prototype.numParams}", + s"vararg=${prototype.isVararg}", + s"max_stack=${prototype.maxStack}", + s"upvalues=${prototype.upvalueCount}" + ).mkString(" ") + } + + private def prototypeSignature(prototype: LuaPrototype): String = + s"(${Vector.fill(prototype.numParams)(Defines.Any).mkString(",")}):${Defines.Any}" + + private def diagnosticCode(result: LuaBytecodeDecodeResult, diagnostic: LuaDiagnostic): String = + Seq( + s"kind=${diagnostic.kind}", + s"severity=${diagnostic.severity}", + s"success_facts_allowed=${diagnostic.successFactsAllowed}", + s"accepted=${result.artifact.accepted}", + s"message=${diagnostic.message}" + ).mkString(" ") + + private def constantValueText(value: LuaConstantValue): String = value match { + case LuaConstantValue.NilValue => "nil" + case LuaConstantValue.BooleanValue(value) => value.toString + case LuaConstantValue.NumberValue(value) => numericText(value) + case LuaConstantValue.StringValue(value) => value + } + + private def numericText(value: Double): String = + if (value.isWhole) value.toLong.toString else value.toString + + private def prototypeTypeFullName(relativeName: String, prototype: LuaPrototype): String = + s"${prototypeMethodFullName(relativeName, prototype)}:prototype" + + private def prototypeMethodFullName(relativeName: String, prototype: LuaPrototype): String = + s"lua:$relativeName:${prototype.prototypeId}" +} + +object LuaBytecodeModelPass { + final case class DecodedBytecode(relativeName: String, result: LuaBytecodeDecodeResult, order: Int) + + def decodeInputs(config: Config): Vector[DecodedBytecode] = { + val inputRoot = Paths.get(config.inputPath).absolutePathAsString + val bytecodeFiles = SourceFiles.determine( + inputPath = inputRoot, + sourceFileExtensions = Set(".luac"), + ignoredDefaultRegex = Some(config.defaultIgnoredFilesRegex), + ignoredFilesRegex = Some(config.ignoredFilesRegex), + ignoredFilesPath = Some(config.ignoredFiles) + )() + + bytecodeFiles.zipWithIndex.map { case (file, index) => + val relativeName = SourceFiles.toRelativePath(file, inputRoot) + val bytes = Files.readAllBytes(Paths.get(file)) + val result = LuaBytecodeDecoder.decode(relativeName, bytes) + DecodedBytecode(relativeName, result, index + 1) + }.toVector + } + +} diff --git a/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/passes/LuaFileInventoryPass.scala b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/passes/LuaFileInventoryPass.scala new file mode 100644 index 000000000000..c3f46774d7c7 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/main/scala/io/joern/lua2cpg/passes/LuaFileInventoryPass.scala @@ -0,0 +1,29 @@ +package io.joern.lua2cpg.passes + +import io.joern.lua2cpg.Config +import io.joern.x2cpg.SourceFiles +import io.shiftleft.codepropertygraph.generated.Cpg +import io.shiftleft.codepropertygraph.generated.nodes.NewFile +import io.shiftleft.passes.CpgPass +import io.shiftleft.semanticcpg.utils.FileUtil.* + +import java.nio.file.Paths + +class LuaFileInventoryPass(cpg: Cpg, config: Config) extends CpgPass(cpg) { + + override def run(diffGraph: DiffGraphBuilder): Unit = { + val inputRoot = Paths.get(config.inputPath).absolutePathAsString + val luaFiles = SourceFiles.determine( + inputPath = inputRoot, + sourceFileExtensions = Set(".lua"), + ignoredDefaultRegex = Some(config.defaultIgnoredFilesRegex), + ignoredFilesRegex = Some(config.ignoredFilesRegex), + ignoredFilesPath = Some(config.ignoredFiles) + )() + + luaFiles.zipWithIndex.foreach { case (file, index) => + val relativeName = SourceFiles.toRelativePath(file, inputRoot) + diffGraph.addNode(NewFile().name(relativeName).order(index + 1)) + } + } +} diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-constants-call/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-constants-call/input.luac new file mode 100644 index 000000000000..829baa29a56c Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-constants-call/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/malformed-constant.luac b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/malformed-constant.luac new file mode 100644 index 000000000000..6d7172f904f0 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/malformed-constant.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/not-lua-bytecode.bin b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/not-lua-bytecode.bin new file mode 100644 index 000000000000..af3f833d8afd --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/not-lua-bytecode.bin @@ -0,0 +1 @@ +not lua bytecode diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/not-lua-bytecode.luac b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/not-lua-bytecode.luac new file mode 100644 index 000000000000..af3f833d8afd --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/not-lua-bytecode.luac @@ -0,0 +1 @@ +not lua bytecode diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/truncated.luac b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/truncated.luac new file mode 100644 index 000000000000..e6835c0d0272 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/truncated.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/unsupported-profile.luac b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/unsupported-profile.luac new file mode 100644 index 000000000000..8b6872a7b18f Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/unsupported-profile.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/unsupported-version.luac b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/unsupported-version.luac new file mode 100644 index 000000000000..29d54d5f8578 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-malformed-diagnostic/unsupported-version.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-prototype-params/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-prototype-params/input.luac new file mode 100644 index 000000000000..8bce7bc47c7e Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-prototype-params/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-stripped-metadata/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-stripped-metadata/input.luac new file mode 100644 index 000000000000..7cd65439088b Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/bytecode-model/bc-stripped-metadata/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/SAMPLE-MANIFEST.md b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/SAMPLE-MANIFEST.md new file mode 100644 index 000000000000..b91465ae54c4 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/SAMPLE-MANIFEST.md @@ -0,0 +1,31 @@ +# Interprocedural Module Taint Samples + +Source family: committed Lua 5.1 bytecode fixtures covering interprocedural +module and taint behavior. + +Positive samples: +- d16-rf-interprocedural-formvalue-execute/input.luac: resolved same-artifact arg/return. +- d16-rf-webcmd-cross-module-popen/controller.luac and mtkwifi.luac: require/module/export/cross-module path. +- d24-module-return-table-field-call/controller.luac and library.luac: returned table field call target. +- bc-taint-minimal-path/input.luac: minimal same-artifact taint path. +- table-field-provenance-positive/input.luac: same fixed table key preserves taint provenance. +- conditional-merge-positive/input.luac: an optional overwrite preserves the bypassed source at branch merge. +- control-selection-positive/input.luac: a tainted predicate controls selection of a value consumed by a sink. + +Negative samples: +- d24-interproc-unresolved-callee-negative/input.luac: unresolved callee boundary. +- d24-module-ambiguous-unresolved-dynamic-negative/*.luac: missing, ambiguous, and dynamic require boundaries. +- d24-module-missing-field-negative/*.luac: missing export field boundary. +- bc-kill-overwrite/input.luac and bc-branch-negative/input.luac: killed/no-flow taint boundaries. +- table-field-provenance-negative/input.luac: distinct fixed table keys do not share taint provenance. +- nested-branch-overwrite-negative/input.luac: an entered branch overwrite kills the prior source before its sink. +- control-unrelated-negative/input.luac: a predicate does not taint assignments outside its controlled region. +- control-overwrite-negative/input.luac: a post-merge overwrite kills prior control-dependent selection. + +Reviewer command: +JAVA_TOOL_OPTIONS='-Dsbt.watch.mode=polling -Dsbt.io.jdktimestamps=true' sbt 'lua2cpg/testOnly io.joern.lua2cpg.InterproceduralModuleTaintSmokeTest' +JAVA_TOOL_OPTIONS='-Dsbt.watch.mode=polling -Dsbt.io.jdktimestamps=true' sbt 'lua2cpg/stage' +git status --short + +This subset is sufficient for reviewer smoke of interprocedural module and +taint semantics and is consumed entirely from the `lua2cpg` test resources. diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bc-branch-negative/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bc-branch-negative/input.luac new file mode 100644 index 000000000000..29c4dfe7ad77 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bc-branch-negative/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bc-kill-overwrite/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bc-kill-overwrite/input.luac new file mode 100644 index 000000000000..7ecd2a1f96b9 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bc-kill-overwrite/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bc-taint-minimal-path/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bc-taint-minimal-path/input.luac new file mode 100644 index 000000000000..39a8a1b6f4e9 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bc-taint-minimal-path/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bc-taint-minimal-path/source.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bc-taint-minimal-path/source.lua new file mode 100644 index 000000000000..5442625f707f --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bc-taint-minimal-path/source.lua @@ -0,0 +1,2 @@ +local value = luci.http.formvalue("value") +os.execute(value) diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bridge-flow-generic/bridge.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bridge-flow-generic/bridge.lua new file mode 100644 index 000000000000..9ac9c421a500 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bridge-flow-generic/bridge.lua @@ -0,0 +1,7 @@ +local M = {} + +function M.forward(value) + return value +end + +return M diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bridge-flow-generic/bridge.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bridge-flow-generic/bridge.luac new file mode 100644 index 000000000000..bcbfe34a0185 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bridge-flow-generic/bridge.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bridge-flow-generic/controller.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bridge-flow-generic/controller.lua new file mode 100644 index 000000000000..c33b912646c4 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bridge-flow-generic/controller.lua @@ -0,0 +1,5 @@ +local bridge = require("bridge") +local command = luci.http.formvalue("cmd") +local transformed = bridge.forward(command) + +return os.execute(transformed) diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bridge-flow-generic/controller.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bridge-flow-generic/controller.luac new file mode 100644 index 000000000000..1764e5110782 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/bridge-flow-generic/controller.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/call-context-negative/input.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/call-context-negative/input.lua new file mode 100644 index 000000000000..798210e20e7f --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/call-context-negative/input.lua @@ -0,0 +1,13 @@ +local function identity(value) + return value +end + +function source_branch() + local tainted = luci.http.formvalue("command") + return identity(tainted) +end + +function safe_sink_branch() + local safe = identity("fixed") + return os.execute(safe) +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/call-context-negative/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/call-context-negative/input.luac new file mode 100644 index 000000000000..d1e7763e454d Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/call-context-negative/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/conditional-merge-positive/input.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/conditional-merge-positive/input.lua new file mode 100644 index 000000000000..b90e178ae09f --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/conditional-merge-positive/input.lua @@ -0,0 +1,7 @@ +local value = luci.http.formvalue("command") + +if use_default then + value = "fixed" +end + +return os.execute(value) diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/conditional-merge-positive/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/conditional-merge-positive/input.luac new file mode 100644 index 000000000000..f0e9aeee71f6 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/conditional-merge-positive/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-overwrite-negative/input.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-overwrite-negative/input.lua new file mode 100644 index 000000000000..43605f131c9e --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-overwrite-negative/input.lua @@ -0,0 +1,10 @@ +local selector = luci.http.formvalue("selector") +local candidate = "selected-command" +local selected = "fixed" + +if selector == "chosen" then + selected = candidate +end + +selected = "fixed" +return os.execute(selected) diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-overwrite-negative/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-overwrite-negative/input.luac new file mode 100644 index 000000000000..ce582b64f5fe Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-overwrite-negative/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-selection-positive/input.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-selection-positive/input.lua new file mode 100644 index 000000000000..ab65e3506a0c --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-selection-positive/input.lua @@ -0,0 +1,9 @@ +local selector = luci.http.formvalue("selector") +local candidate = "selected-command" +local selected = "fixed" + +if selector == "chosen" then + selected = candidate +end + +return os.execute(selected) diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-selection-positive/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-selection-positive/input.luac new file mode 100644 index 000000000000..bfef1702895f Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-selection-positive/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-unrelated-negative/input.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-unrelated-negative/input.lua new file mode 100644 index 000000000000..051516ea9b27 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-unrelated-negative/input.lua @@ -0,0 +1,9 @@ +local selector = luci.http.formvalue("selector") +local ignored = "fixed" + +if selector == "chosen" then + ignored = "ignored" +end + +local selected = "fixed" +return os.execute(selected) diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-unrelated-negative/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-unrelated-negative/input.luac new file mode 100644 index 000000000000..87c9a2c7cba0 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/control-unrelated-negative/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d16-rf-interprocedural-formvalue-execute/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d16-rf-interprocedural-formvalue-execute/input.luac new file mode 100644 index 000000000000..dba27679d25a Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d16-rf-interprocedural-formvalue-execute/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d16-rf-webcmd-cross-module-popen/controller.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d16-rf-webcmd-cross-module-popen/controller.luac new file mode 100644 index 000000000000..adc8690d65c8 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d16-rf-webcmd-cross-module-popen/controller.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d16-rf-webcmd-cross-module-popen/mtkwifi.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d16-rf-webcmd-cross-module-popen/mtkwifi.luac new file mode 100644 index 000000000000..34aa80ab8ba7 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d16-rf-webcmd-cross-module-popen/mtkwifi.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-interproc-unresolved-callee-negative/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-interproc-unresolved-callee-negative/input.luac new file mode 100644 index 000000000000..42f9fca2d6dc Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-interproc-unresolved-callee-negative/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/ambiguous.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/ambiguous.luac new file mode 100644 index 000000000000..ff52b914488f Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/ambiguous.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/controller.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/controller.luac new file mode 100644 index 000000000000..515c17561933 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/controller.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/left.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/left.lua new file mode 100644 index 000000000000..90c21f9b21d3 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/left.lua @@ -0,0 +1,5 @@ +module("shared.module") + +function exec(cmd) + return io.popen(cmd) +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/left.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/left.luac new file mode 100644 index 000000000000..9fb9a7f4b5d3 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/left.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/missing.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/missing.luac new file mode 100644 index 000000000000..75da7ae6df53 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/missing.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/right.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/right.lua new file mode 100644 index 000000000000..90c21f9b21d3 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/right.lua @@ -0,0 +1,5 @@ +module("shared.module") + +function exec(cmd) + return io.popen(cmd) +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/right.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/right.luac new file mode 100644 index 000000000000..6363938f3b79 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-ambiguous-unresolved-dynamic-negative/right.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-missing-field-negative/controller.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-missing-field-negative/controller.lua new file mode 100644 index 000000000000..d9c64ec8fea1 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-missing-field-negative/controller.lua @@ -0,0 +1,7 @@ +local lib = require("missinglib") + +local function run() + return lib.missing("whoami") +end + +return run diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-missing-field-negative/controller.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-missing-field-negative/controller.luac new file mode 100644 index 000000000000..7f54a07ab907 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-missing-field-negative/controller.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-missing-field-negative/missinglib.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-missing-field-negative/missinglib.lua new file mode 100644 index 000000000000..5ba42fc9395c --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-missing-field-negative/missinglib.lua @@ -0,0 +1,7 @@ +local M = {} + +function M.run(cmd) + return io.popen(cmd) +end + +return M diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-missing-field-negative/missinglib.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-missing-field-negative/missinglib.luac new file mode 100644 index 000000000000..24c16ee25868 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-missing-field-negative/missinglib.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-return-table-field-call/controller.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-return-table-field-call/controller.lua new file mode 100644 index 000000000000..f9a33a6bf9e1 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-return-table-field-call/controller.lua @@ -0,0 +1,15 @@ +local function via_local() + local lib = require("returnlib") + local cmd = luci.http.formvalue("cmd") + return lib.run(cmd) +end + +local function via_direct() + local cmd = luci.http.formvalue("cmd") + return require("returnlib").run(cmd) +end + +return { + via_local = via_local, + via_direct = via_direct, +} diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-return-table-field-call/controller.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-return-table-field-call/controller.luac new file mode 100644 index 000000000000..456de8fd9bd1 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-return-table-field-call/controller.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-return-table-field-call/returnlib.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-return-table-field-call/returnlib.lua new file mode 100644 index 000000000000..1fe13e23c9eb --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-return-table-field-call/returnlib.lua @@ -0,0 +1,7 @@ +local M = {} + +function M.run(cmd) + return os.execute(cmd) +end + +return M diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-return-table-field-call/returnlib.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-return-table-field-call/returnlib.luac new file mode 100644 index 000000000000..ddb249cf17c9 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/d24-module-return-table-field-call/returnlib.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/a/foo.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/a/foo.lua new file mode 100644 index 000000000000..14de4f551593 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/a/foo.lua @@ -0,0 +1 @@ +return { selected = "a" } diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/a/foo.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/a/foo.luac new file mode 100644 index 000000000000..b3456856f4ce Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/a/foo.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/b/foo.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/b/foo.lua new file mode 100644 index 000000000000..cffd4514930b --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/b/foo.lua @@ -0,0 +1 @@ +return { selected = "b" } diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/b/foo.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/b/foo.luac new file mode 100644 index 000000000000..f8809c2f89e9 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/b/foo.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/controller.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/controller.lua new file mode 100644 index 000000000000..ef00083024aa --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/controller.lua @@ -0,0 +1,7 @@ +local first = require("foo") +local util = require("luci.util") + +return { + first = first, + util = util, +} diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/controller.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/controller.luac new file mode 100644 index 000000000000..6c4d61fb5933 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/controller.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/vendor/luci/util.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/vendor/luci/util.lua new file mode 100644 index 000000000000..9bb1c47b66f9 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/vendor/luci/util.lua @@ -0,0 +1 @@ +return { selected = "util" } diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/vendor/luci/util.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/vendor/luci/util.luac new file mode 100644 index 000000000000..4054203490f8 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/vendor/luci/util.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/z/deep/foo/init.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/z/deep/foo/init.lua new file mode 100644 index 000000000000..38ef5d22f1fc --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/z/deep/foo/init.lua @@ -0,0 +1 @@ +return { selected = "init" } diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/z/deep/foo/init.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/z/deep/foo/init.luac new file mode 100644 index 000000000000..aab948e2da37 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/module-resolution-generic/z/deep/foo/init.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/nested-branch-overwrite-negative/input.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/nested-branch-overwrite-negative/input.lua new file mode 100644 index 000000000000..112863c4636a --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/nested-branch-overwrite-negative/input.lua @@ -0,0 +1,6 @@ +local value = luci.http.formvalue("command") + +if enabled then + value = "fixed" + return os.execute(value) +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/nested-branch-overwrite-negative/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/nested-branch-overwrite-negative/input.luac new file mode 100644 index 000000000000..cec5d8ab423c Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/nested-branch-overwrite-negative/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/table-field-provenance-negative/input.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/table-field-provenance-negative/input.lua new file mode 100644 index 000000000000..18748818d03e --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/table-field-provenance-negative/input.lua @@ -0,0 +1,4 @@ +local box = {} +box.metadata = luci.http.formvalue("metadata") + +return os.execute(box.command) diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/table-field-provenance-negative/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/table-field-provenance-negative/input.luac new file mode 100644 index 000000000000..1e7964f9ed21 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/table-field-provenance-negative/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/table-field-provenance-positive/input.lua b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/table-field-provenance-positive/input.lua new file mode 100644 index 000000000000..6ef87ea1793d --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/table-field-provenance-positive/input.lua @@ -0,0 +1,4 @@ +local box = {} +box.command = luci.http.formvalue("command") + +return os.execute(box.command) diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/table-field-provenance-positive/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/table-field-provenance-positive/input.luac new file mode 100644 index 000000000000..c24e19c08f27 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/interprocedural-module-taint/table-field-provenance-positive/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/bc-call-candidate-unresolved/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/bc-call-candidate-unresolved/input.luac new file mode 100644 index 000000000000..9672d77f6b77 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/bc-call-candidate-unresolved/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/bc-kill-overwrite/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/bc-kill-overwrite/input.luac new file mode 100644 index 000000000000..7ecd2a1f96b9 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/bc-kill-overwrite/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/bc-table-global-upvalue/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/bc-table-global-upvalue/input.luac new file mode 100644 index 000000000000..9bbb5ed371ef Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/bc-table-global-upvalue/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/d24-defuse-transitive-chain/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/d24-defuse-transitive-chain/input.luac new file mode 100644 index 000000000000..33f82f48d2da Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/d24-defuse-transitive-chain/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/d24-defuse-unrelated-register-negative/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/d24-defuse-unrelated-register-negative/input.luac new file mode 100644 index 000000000000..a7f53bfc1ba4 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/d24-defuse-unrelated-register-negative/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/d24-global-dynamic-env-negative/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/d24-global-dynamic-env-negative/input.luac new file mode 100644 index 000000000000..7a93f1790026 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/d24-global-dynamic-env-negative/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/d24-table-dynamic-key-negative/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/d24-table-dynamic-key-negative/input.luac new file mode 100644 index 000000000000..cb88e46c3af7 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/d24-table-dynamic-key-negative/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/d24-upvalue-mutation-negative/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/d24-upvalue-mutation-negative/input.luac new file mode 100644 index 000000000000..b12ecda4b10d Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/d24-upvalue-mutation-negative/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/local-value-flow-generic/input.lua b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/local-value-flow-generic/input.lua new file mode 100644 index 000000000000..25b420ddeeb5 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/local-value-flow-generic/input.lua @@ -0,0 +1,26 @@ +local function alias_flow(input) + local box = {} + box.value = input + local alias = box + return os.execute(alias.value) +end + +local function overwrite_flow(input) + local box = {} + box.value = input + box.value = "safe" + return os.execute(box.value) +end + +local function conditional_parameter(input, use_default) + if use_default then + input = "safe" + end + return os.execute(input) +end + +return { + alias_flow = alias_flow, + overwrite_flow = overwrite_flow, + conditional_parameter = conditional_parameter, +} diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/local-value-flow-generic/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/local-value-flow-generic/input.luac new file mode 100644 index 000000000000..ff7ddae68899 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/intraprocedural-semantics/local-value-flow-generic/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/.gitattributes b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/.gitattributes new file mode 100644 index 000000000000..1b673dbd32d0 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/.gitattributes @@ -0,0 +1,4 @@ +usr/lib/lua/*.lua -text -diff +usr/lib/lua/**/*.lua -text -diff +usr/lib/lua/*.luac -text -diff +usr/lib/lua/**/*.luac -text -diff diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/SAMPLE-MANIFEST.md b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/SAMPLE-MANIFEST.md new file mode 100644 index 000000000000..27f70f963cda --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/SAMPLE-MANIFEST.md @@ -0,0 +1,26 @@ +# Lua2Cpg OpenWrt-Derived Firmware Lua Fixture Corpus + +This committed fixture corpus contains Lua files extracted from the +`usr/lib/lua` tree of a real-device OpenWrt-derived firmware image. It is used +by `lua2cpg` tests as a self-contained reviewer corpus for Lua 5.1 source file +inventory, Lua 5.1 bytecode decoding, CPG generation, and native taint evidence +export. + +The outer resource name and test descriptions are vendor-neutral. The original +relative paths and file bytes below `usr/lib/lua` are preserved. + +## Contents + +- Scope: `usr/lib/lua` +- Lua source files: 42 +- Lua bytecode files: 42 +- Total `.lua` and `.luac` files: 84 + +## Notes + +- The `.lua` files exercise source file inventory in the CPG. +- The `.luac` files exercise the bytecode-based semantic and taint-analysis + pipeline. +- This corpus is consumed entirely from `lua2cpg` test resources. +- This manifest does not assert licensing or redistribution facts beyond the + presence of the committed test files. diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/l1dat_parser.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/l1dat_parser.lua new file mode 100755 index 000000000000..4763028cb279 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/l1dat_parser.lua @@ -0,0 +1,349 @@ +#!/usr/bin/env lua + +--[[ + * A lua library to manipulate mtk's wifi driver. used in luci-app-mtk. + * + * Copyright (C) 2016 MTK + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +]] + +local l1dat_parser = { + L1_DAT_PATH = "/etc/wireless/l1profile.dat", + IF_RINDEX = "ifname_ridx", + DEV_RINDEX = "devname_ridx", + MAX_NUM_APCLI = 1, + MAX_NUM_WDS = 4, + MAX_NUM_MESH = 1, + MAX_NUM_EXTIF = 16, + MAX_NUM_DBDC_BAND = 2, +} + +local l1cfg_options = { + ext_ifname="", + apcli_ifname="apcli", + wds_ifname="wds", + mesh_ifname="mesh" + } + +function l1dat_parser.__trim(s) + if s then return (s:gsub("^%s*(.-)%s*$", "%1")) end +end + +function l1dat_parser.__cfg2list(str) + -- delimeter == ";" + local i = 1 + local list = {} + for k in string.gmatch(str, "([^;]+)") do + list[i] = k + i = i + 1 + end + return list +end + +function l1dat_parser.token_get(str, n, v) + -- n starts from 1 + -- v is the backup in case token n is nil + if not str then return v end + local tmp = l1dat_parser.__cfg2list(str) + return tmp[tonumber(n)] or v +end + +function l1dat_parser.add_default_value(l1cfg) + for k, v in ipairs(l1cfg) do + + for opt, default in pairs(l1cfg_options) do + if ( opt == "ext_ifname" ) then + v[opt] = v[opt] or v["main_ifname"].."_" + else + v[opt] = v[opt] or default..k.."_" + end + end + end + + return l1cfg +end + +function l1dat_parser.get_value_by_idx(devidx, mainidx, subidx, key) + --print("Enter l1dat_parser.get_value_by_idx("..devidx..","..mainidx..", "..subidx..", "..key..")
") + if not devidx or not mainidx or not key then return end + + local devs = l1dat_parser.load_l1_profile(l1dat_parser.L1_DAT_PATH) + if not devs then return end + + local dev_ridx = l1dat_parser.DEV_RINDEX + local sidx = subidx or 1 + local devname1 = devidx.."."..mainidx + local devname2 = devidx.."."..mainidx.."."..sidx + + --print("devnam1=", devname1, "devname2=", devname2, "
") + return devs[dev_ridx][devname2] and devs[dev_ridx][devname2][key] + or devs[dev_ridx][devname1] and devs[dev_ridx][devname1][key] +end + +-- path to zone is 1 to 1 mapping +function l1dat_parser.l1_path_to_zone(path) + --print("Enter l1dat_parser.l1_path_to_zone("..path..")
") + if not path then return end + + local devs = l1dat_parser.load_l1_profile(l1dat_parser.L1_DAT_PATH) + if not devs then return end + + for _, dev in pairs(devs[l1dat_parser.IF_RINDEX]) do + if dev.profile_path == path then + return dev.nvram_zone + end + end + + return +end + +-- zone to path is 1 to n mapping +function l1dat_parser.l1_zone_to_path(zone) + if not zone then return end + + local devs = l1dat_parser.load_l1_profile(l1dat_parser.L1_DAT_PATH) + if not devs then return end + + local plist = {} + for _, dev in pairs(devs[l1dat_parser.IF_RINDEX]) do + if dev.nvram_zone == zone then + if not next(plist) then + table.insert(plist,dev.profile_path) + else + local plist_str = table.concat(plist) + if not plist_str:match(dev.profile_path) then + table.insert(plist,dev.profile_path) + end + end + end + end + + return next(plist) and plist or nil +end + +function l1dat_parser.l1_ifname_to_datpath(ifname) + if not ifname then return end + + local devs = l1dat_parser.load_l1_profile(l1dat_parser.L1_DAT_PATH) + if not devs then return end + + local ridx = l1dat_parser.IF_RINDEX + return devs[ridx][ifname] and devs[ridx][ifname].profile_path +end + +function l1dat_parser.l1_ifname_to_zone(ifname) + if not ifname then return end + + local devs = l1dat_parser.load_l1_profile(l1dat_parser.L1_DAT_PATH) + if not devs then return end + + local ridx = l1dat_parser.IF_RINDEX + return devs[ridx][ifname] and devs[ridx][ifname].nvram_zone +end + +function l1dat_parser.l1_zone_to_ifname(zone) + if not zone then return end + + local devs = l1dat_parser.load_l1_profile(l1dat_parser.L1_DAT_PATH) + if not devs then return end + + local zone_dev + for _, dev in pairs(devs[l1dat_parser.DEV_RINDEX]) do + if dev.nvram_zone == zone then + zone_dev = dev + end + end + + if not zone_dev then + return nil + else + return zone_dev.main_ifname, zone_dev.ext_ifname, zone_dev.apcli_ifname, zone_dev.wds_ifname, zone_dev.mesh_ifname + end +end + +-- input: L1 profile path. +-- output A table, devs, contains +-- 1. devs[%d] = table of each INDEX# in the L1 profile +-- 2. devs.ifname_ridx[ifname] +-- = table of each ifname and point to relevant contain in dev[$d] +-- 3. devs.devname_ridx[devname] similar to devs.ifnameridx, but use devname. +-- devname = INDEX#_value.mainidx(.subidx) +-- Using *_ridx do not need to handle name=k1;k2 case of DBDC card. +function l1dat_parser.load_l1_profile(path) + local devs = setmetatable({}, {__index= + function(tbl, key) + local util = require("luci.util") + --print("metatable function:", util.serialize_data(tbl), key) + --print("-----------------------------------------------") + if ( string.match(key, "^%d+")) then + tbl[key] = {} + return tbl[key] + end + end + }) + local nixio = require("nixio") + local chipset_num = {} + local dir = io.popen("ls /etc/wireless/") + if not dir then return end + local fd = io.open(path, "r") + if not fd then return end + + -- convert l1 profile into lua table + for line in fd:lines() do + line = l1dat_parser.__trim(line) + if string.byte(line) ~= string.byte("#") then + local i = string.find(line, "=") + if i then + local k, v, k1, k2 + k = l1dat_parser.__trim( string.sub(line, 1, i-1) ) + v = l1dat_parser.__trim( string.sub(line, i+1) ) + k1, k2 = string.match(k, "INDEX(%d+)_(.+)") + if k1 then + k1 = tonumber(k1) + 1 + if devs[k1][k2] then + nixio.syslog("warning", "skip repeated key"..line) + end + devs[k1][k2] = v or "" + else + k1 = string.match(k, "INDEX(%d+)") + k1 = tonumber(k1) + 1 + devs[k1]["INDEX"] = v + + chipset_num[v] = (not chipset_num[v] and 1) or chipset_num[v] + 1 + devs[k1]["mainidx"] = chipset_num[v] + end + else + nixio.syslog("warning", "skip line without '=' "..line) + end + else + nixio.syslog("warning", "skip comment line "..line) + end + end + + l1dat_parser.add_default_value(devs) + --local util = require("luci.util") + --local seen2 = {} + -- print("Before setup ridx", util.serialize_data(devs, seen2)) + + -- Force to setup reverse indice for quick search. + -- Benifit: + -- 1. O(1) search with ifname, devname + -- 2. Seperate DBDC name=k1;k2 format in the L1 profile into each + -- ifname, devname. + local dbdc_if = {} + local ridx = l1dat_parser.IF_RINDEX + local dridx = l1dat_parser.DEV_RINDEX + local band_num = l1dat_parser.MAX_NUM_DBDC_BAND + local k, v, dev, i , j, last + local devname + devs[ridx] = {} + devs[dridx] = {} + for _, dev in ipairs(devs) do + dbdc_if[band_num] = l1dat_parser.token_get(dev.main_ifname, band_num, nil) + if dbdc_if[band_num] then + for i = 1, band_num - 1 do + dbdc_if[i] = l1dat_parser.token_get(dev.main_ifname, i, nil) + end + for i = 1, band_num do + devs[ridx][dbdc_if[i]] = {} + devs[ridx][dbdc_if[i]]["subidx"] = i + + for k, v in pairs(dev) do + if k == "INDEX" or k == "EEPROM_offset" or k == "EEPROM_size" + or k == "mainidx" then + devs[ridx][dbdc_if[i]][k] = v + else + devs[ridx][dbdc_if[i]][k] = l1dat_parser.token_get(v, i, "") + end + end + devname = dev.INDEX.."."..dev.mainidx.."."..devs[ridx][dbdc_if[i]]["subidx"] + devs[dridx][devname] = devs[ridx][dbdc_if[i]] + end + + local apcli_if, wds_if, ext_if, mesh_if = {}, {}, {}, {} + + for i = 1, band_num do + ext_if[i] = l1dat_parser.token_get(dev.ext_ifname, i, nil) + apcli_if[i] = l1dat_parser.token_get(dev.apcli_ifname, i, nil) + wds_if[i] = l1dat_parser.token_get(dev.wds_ifname, i, nil) + mesh_if[i] = l1dat_parser.token_get(dev.mesh_ifname, i, nil) + end + + for i = 1, l1dat_parser.MAX_NUM_EXTIF - 1 do -- ifname idx is from 0 + for j = 1, band_num do + devs[ridx][ext_if[j]..i] = devs[ridx][dbdc_if[j]] + end + end + + for i = 0, l1dat_parser.MAX_NUM_APCLI - 1 do + for j = 1, band_num do + devs[ridx][apcli_if[j]..i] = devs[ridx][dbdc_if[j]] + end + end + + for i = 0, l1dat_parser.MAX_NUM_WDS - 1 do + for j = 1, band_num do + devs[ridx][wds_if[j]..i] = devs[ridx][dbdc_if[j]] + end + end + + for i = 0, l1dat_parser.MAX_NUM_MESH - 1 do + for j = 1, band_num do + if mesh_if[j] then + devs[ridx][mesh_if[j]..i] = devs[ridx][dbdc_if[j]] + end + end + end + + else + devs[ridx][dev.main_ifname] = dev + + devname = dev.INDEX.."."..dev.mainidx + devs[dridx][devname] = dev + + for i = 1, l1dat_parser.MAX_NUM_EXTIF - 1 do -- ifname idx is from 0 + devs[ridx][dev.ext_ifname..i] = dev + end + + for i = 0, l1dat_parser.MAX_NUM_APCLI - 1 do -- ifname idx is from 0 + devs[ridx][dev.apcli_ifname..i] = dev + end + + for i = 0, l1dat_parser.MAX_NUM_WDS - 1 do -- ifname idx is from 0 + devs[ridx][dev.wds_ifname..i] = dev + end + + for i = 0, l1dat_parser.MAX_NUM_MESH - 1 do -- ifname idx is from 0 + devs[ridx][dev.mesh_ifname..i] = dev + end + end + end + + fd:close() + return devs +end + +function l1dat_parser.creat_link_for_nvram( ) + local devs = l1dat_parser.load_l1_profile(l1dat_parser.L1_DAT_PATH) + for devname, dev in pairs(devs.devname_ridx) do + local dev = devs.devname_ridx[devname] + profile = dev.profile_path + os.execute("mkdir -p /tmp/mtk/wifi/") + if dev.nvram_zone == "dev1" then + os.execute("ln -sf " ..profile.." /tmp/mtk/wifi/2860") + elseif dev.nvram_zone == "dev2" then + os.execute("ln -sf " ..profile.." /tmp/mtk/wifi/rtdev") + elseif dev.nvram_zone == "dev3" then + os.execute("ln -sf " ..profile.." /tmp/mtk/wifi/wifi3") + end + end +end +return l1dat_parser diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/l1dat_parser.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/l1dat_parser.luac new file mode 100644 index 000000000000..b09ee80a4c32 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/l1dat_parser.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/ltn12.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/ltn12.lua new file mode 100644 index 000000000000..afa735dc2cec --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/ltn12.lua @@ -0,0 +1,319 @@ +----------------------------------------------------------------------------- +-- LTN12 - Filters, sources, sinks and pumps. +-- LuaSocket toolkit. +-- Author: Diego Nehab +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module +----------------------------------------------------------------------------- +local string = require("string") +local table = require("table") +local unpack = unpack or table.unpack +local base = _G +local _M = {} +if module then -- heuristic for exporting a global package table + ltn12 = _M +end +local filter,source,sink,pump = {},{},{},{} + +_M.filter = filter +_M.source = source +_M.sink = sink +_M.pump = pump + +local unpack = unpack or table.unpack +local select = base.select + +-- 2048 seems to be better in windows... +_M.BLOCKSIZE = 2048 +_M._VERSION = "LTN12 1.0.3" + +----------------------------------------------------------------------------- +-- Filter stuff +----------------------------------------------------------------------------- +-- returns a high level filter that cycles a low-level filter +function filter.cycle(low, ctx, extra) + base.assert(low) + return function(chunk) + local ret + ret, ctx = low(ctx, chunk, extra) + return ret + end +end + +-- chains a bunch of filters together +-- (thanks to Wim Couwenberg) +function filter.chain(...) + local arg = {...} + local n = base.select('#',...) + local top, index = 1, 1 + local retry = "" + return function(chunk) + retry = chunk and retry + while true do + if index == top then + chunk = arg[index](chunk) + if chunk == "" or top == n then return chunk + elseif chunk then index = index + 1 + else + top = top+1 + index = top + end + else + chunk = arg[index](chunk or "") + if chunk == "" then + index = index - 1 + chunk = retry + elseif chunk then + if index == n then return chunk + else index = index + 1 end + else base.error("filter returned inappropriate nil") end + end + end + end +end + +----------------------------------------------------------------------------- +-- Source stuff +----------------------------------------------------------------------------- +-- create an empty source +local function empty() + return nil +end + +function source.empty() + return empty +end + +-- returns a source that just outputs an error +function source.error(err) + return function() + return nil, err + end +end + +-- creates a file source +function source.file(handle, io_err) + if handle then + return function() + local chunk = handle:read(_M.BLOCKSIZE) + if not chunk then handle:close() end + return chunk + end + else return source.error(io_err or "unable to open file") end +end + +-- turns a fancy source into a simple source +function source.simplify(src) + base.assert(src) + return function() + local chunk, err_or_new = src() + src = err_or_new or src + if not chunk then return nil, err_or_new + else return chunk end + end +end + +-- creates string source +function source.string(s) + if s then + local i = 1 + return function() + local chunk = string.sub(s, i, i+_M.BLOCKSIZE-1) + i = i + _M.BLOCKSIZE + if chunk ~= "" then return chunk + else return nil end + end + else return source.empty() end +end + +-- creates table source +function source.table(t) + base.assert('table' == type(t)) + local i = 0 + return function() + i = i + 1 + return t[i] + end +end + +-- creates rewindable source +function source.rewind(src) + base.assert(src) + local t = {} + return function(chunk) + if not chunk then + chunk = table.remove(t) + if not chunk then return src() + else return chunk end + else + table.insert(t, chunk) + end + end +end + +-- chains a source with one or several filter(s) +function source.chain(src, f, ...) + if ... then f=filter.chain(f, ...) end + base.assert(src and f) + local last_in, last_out = "", "" + local state = "feeding" + local err + return function() + if not last_out then + base.error('source is empty!', 2) + end + while true do + if state == "feeding" then + last_in, err = src() + if err then return nil, err end + last_out = f(last_in) + if not last_out then + if last_in then + base.error('filter returned inappropriate nil') + else + return nil + end + elseif last_out ~= "" then + state = "eating" + if last_in then last_in = "" end + return last_out + end + else + last_out = f(last_in) + if last_out == "" then + if last_in == "" then + state = "feeding" + else + base.error('filter returned ""') + end + elseif not last_out then + if last_in then + base.error('filter returned inappropriate nil') + else + return nil + end + else + return last_out + end + end + end + end +end + +-- creates a source that produces contents of several sources, one after the +-- other, as if they were concatenated +-- (thanks to Wim Couwenberg) +function source.cat(...) + local arg = {...} + local src = table.remove(arg, 1) + return function() + while src do + local chunk, err = src() + if chunk then return chunk end + if err then return nil, err end + src = table.remove(arg, 1) + end + end +end + +----------------------------------------------------------------------------- +-- Sink stuff +----------------------------------------------------------------------------- +-- creates a sink that stores into a table +function sink.table(t) + t = t or {} + local f = function(chunk, err) + if chunk then table.insert(t, chunk) end + return 1 + end + return f, t +end + +-- turns a fancy sink into a simple sink +function sink.simplify(snk) + base.assert(snk) + return function(chunk, err) + local ret, err_or_new = snk(chunk, err) + if not ret then return nil, err_or_new end + snk = err_or_new or snk + return 1 + end +end + +-- creates a file sink +function sink.file(handle, io_err) + if handle then + return function(chunk, err) + if not chunk then + handle:close() + return 1 + else return handle:write(chunk) end + end + else return sink.error(io_err or "unable to open file") end +end + +-- creates a sink that discards data +local function null() + return 1 +end + +function sink.null() + return null +end + +-- creates a sink that just returns an error +function sink.error(err) + return function() + return nil, err + end +end + +-- chains a sink with one or several filter(s) +function sink.chain(f, snk, ...) + if ... then + local args = { f, snk, ... } + snk = table.remove(args, #args) + f = filter.chain(unpack(args)) + end + base.assert(f and snk) + return function(chunk, err) + if chunk ~= "" then + local filtered = f(chunk) + local done = chunk and "" + while true do + local ret, snkerr = snk(filtered, err) + if not ret then return nil, snkerr end + if filtered == done then return 1 end + filtered = f(done) + end + else return 1 end + end +end + +----------------------------------------------------------------------------- +-- Pump stuff +----------------------------------------------------------------------------- +-- pumps one chunk from the source to the sink +function pump.step(src, snk) + local chunk, src_err = src() + local ret, snk_err = snk(chunk, src_err) + if chunk and ret then return 1 + else return nil, src_err or snk_err end +end + +-- pumps all data from a source to a sink, using a step function +function pump.all(src, snk, step) + base.assert(src and snk) + step = step or pump.step + while true do + local ret, err = step(src, snk) + if not ret then + if err then return nil, err + else return 1 end + end + end +end + +return _M diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/ltn12.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/ltn12.luac new file mode 100644 index 000000000000..8ac213298f7d Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/ltn12.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/cacheloader.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/cacheloader.lua new file mode 100644 index 000000000000..7ef971df8dae --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/cacheloader.lua @@ -0,0 +1,12 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2008 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. + +local config = require "luci.config" +local ccache = require "luci.ccache" + +module "luci.cacheloader" + +if config.ccache and config.ccache.enable == "1" then + ccache.cache_ondemand() +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/cacheloader.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/cacheloader.luac new file mode 100644 index 000000000000..a4549a694788 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/cacheloader.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/ccache.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/ccache.lua new file mode 100644 index 000000000000..d3be7cba6c64 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/ccache.lua @@ -0,0 +1,76 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2008 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. + +local io = require "io" +local fs = require "nixio.fs" +local util = require "luci.util" +local nixio = require "nixio" +local debug = require "debug" +local string = require "string" +local package = require "package" + +local type, loadfile = type, loadfile + + +module "luci.ccache" + +function cache_ondemand(...) + if debug.getinfo(1, 'S').source ~= "=?" then + cache_enable(...) + end +end + +function cache_enable(cachepath, mode) + cachepath = cachepath or "/tmp/luci-modulecache" + mode = mode or "r--r--r--" + + local loader = package.loaders[2] + local uid = nixio.getuid() + + if not fs.stat(cachepath) then + fs.mkdir(cachepath) + end + + local function _encode_filename(name) + local encoded = "" + for i=1, #name do + encoded = encoded .. ("%2X" % string.byte(name, i)) + end + return encoded + end + + local function _load_sane(file) + local stat = fs.stat(file) + if stat and stat.uid == uid and stat.modestr == mode then + return loadfile(file) + end + end + + local function _write_sane(file, func) + if nixio.getuid() == uid then + local fp = io.open(file, "w") + if fp then + fp:write(util.get_bytecode(func)) + fp:close() + fs.chmod(file, mode) + end + end + end + + package.loaders[2] = function(mod) + local encoded = cachepath .. "/" .. _encode_filename(mod) + local modcons = _load_sane(encoded) + + if modcons then + return modcons + end + + -- No cachefile + modcons = loader(mod) + if type(modcons) == "function" then + _write_sane(encoded, modcons) + end + return modcons + end +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/ccache.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/ccache.luac new file mode 100644 index 000000000000..542c00e15009 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/ccache.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/config.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/config.lua new file mode 100644 index 000000000000..d01153f4f564 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/config.lua @@ -0,0 +1,18 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +local util = require "luci.util" +module("luci.config", + function(m) + if pcall(require, "luci.model.uci") then + local config = util.threadlocal() + setmetatable(m, { + __index = function(tbl, key) + if not config[key] then + config[key] = luci.model.uci.cursor():get_all("luci", key) + end + return config[key] + end + }) + end + end) diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/config.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/config.luac new file mode 100644 index 000000000000..64a2cbcc44b5 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/config.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/admin/index.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/admin/index.lua new file mode 100644 index 000000000000..736d0cdccff3 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/admin/index.lua @@ -0,0 +1,196 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +module("luci.controller.admin.index", package.seeall) + +function action_logout() + local dsp = require "luci.dispatcher" + local utl = require "luci.util" + local sid = dsp.context.authsession + + if sid then + utl.ubus("session", "destroy", { ubus_rpc_session = sid }) + + luci.http.header("Set-Cookie", "sysauth=%s; expires=%s; path=%s" %{ + '', 'Thu, 01 Jan 1970 01:00:00 GMT', dsp.build_url() + }) + end + + luci.http.redirect(dsp.build_url()) +end + +function action_translations(lang) + local i18n = require "luci.i18n" + local http = require "luci.http" + local fs = require "nixio".fs + + if lang and #lang > 0 then + lang = i18n.setlanguage(lang) + if lang then + local s = fs.stat("%s/base.%s.lmo" %{ i18n.i18ndir, lang }) + if s then + http.header("Cache-Control", "public, max-age=31536000") + http.header("ETag", "%x-%x-%x" %{ s["ino"], s["size"], s["mtime"] }) + end + end + end + + http.prepare_content("application/javascript; charset=utf-8") + http.write("window.TR=") + http.write_json(i18n.dump()) +end + +local function ubus_reply(id, data, code, errmsg) + local reply = { jsonrpc = "2.0", id = id } + if errmsg then + reply.error = { + code = code, + message = errmsg + } + elseif type(code) == "table" then + reply.result = code + else + reply.result = { code, data } + end + + return reply +end + +local ubus_types = { + nil, + "array", + "object", + "string", + nil, -- INT64 + "number", + nil, -- INT16, + "boolean", + "double" +} + +local function ubus_access(sid, obj, fun) + local res, code = luci.util.ubus("session", "access", { + ubus_rpc_session = sid, + scope = "ubus", + object = obj, + ["function"] = fun + }) + + return (type(res) == "table" and res.access == true) +end + +local function ubus_request(req) + if type(req) ~= "table" or type(req.method) ~= "string" or req.jsonrpc ~= "2.0" or req.id == nil then + return ubus_reply(nil, nil, -32600, "Invalid request") + + elseif req.method == "call" then + if type(req.params) ~= "table" or #req.params < 3 then + return ubus_reply(nil, nil, -32600, "Invalid parameters") + end + + local sid, obj, fun, arg = + req.params[1], req.params[2], req.params[3], req.params[4] or {} + if type(arg) ~= "table" or arg.ubus_rpc_session ~= nil then + return ubus_reply(req.id, nil, -32602, "Invalid parameters") + end + + if sid == "00000000000000000000000000000000" and luci.dispatcher.context.authsession then + sid = luci.dispatcher.context.authsession + end + + if not ubus_access(sid, obj, fun) then + return ubus_reply(req.id, nil, -32002, "Access denied") + end + + arg.ubus_rpc_session = sid + + local res, code = luci.util.ubus(obj, fun, arg) + return ubus_reply(req.id, res, code or 0) + + elseif req.method == "list" then + if req.params == nil or (type(req.params) == "table" and #req.params == 0) then + local objs = luci.util.ubus() + return ubus_reply(req.id, nil, objs) + + elseif type(req.params) == "table" then + local n, rv = nil, {} + for n = 1, #req.params do + if type(req.params[n]) ~= "string" then + return ubus_reply(req.id, nil, -32602, "Invalid parameters") + end + + local sig = luci.util.ubus(req.params[n]) + if sig and type(sig) == "table" then + rv[req.params[n]] = {} + + local m, p + for m, p in pairs(sig) do + if type(p) == "table" then + rv[req.params[n]][m] = {} + + local pn, pt + for pn, pt in pairs(p) do + rv[req.params[n]][m][pn] = ubus_types[pt] or "unknown" + end + end + end + end + end + return ubus_reply(req.id, nil, rv) + + else + return ubus_reply(req.id, nil, -32602, "Invalid parameters") + end + end + + return ubus_reply(req.id, nil, -32601, "Method not found") +end + +function action_ubus() + local parser = require "luci.jsonc".new() + + luci.http.context.request:setfilehandler(function(_, s) + if not s then + return nil + end + + local ok, err = parser:parse(s) + return (not err or nil) + end) + + luci.http.context.request:content() + + local json = parser:get() + if json == nil or type(json) ~= "table" then + luci.http.prepare_content("application/json") + luci.http.write_json(ubus_reply(nil, nil, -32700, "Parse error")) + return + end + + local response + if #json == 0 then + response = ubus_request(json) + else + response = {} + + local _, request + for _, request in ipairs(json) do + response[_] = ubus_request(request) + end + end + + luci.http.prepare_content("application/json") + luci.http.write_json(response) +end + +function action_menu() + local dsp = require "luci.dispatcher" + local utl = require "luci.util" + local http = require "luci.http" + + local acls = utl.ubus("session", "access", { ubus_rpc_session = http.getcookie("sysauth") }) + local menu = dsp.menu_json(acls or {}) or {} + + http.prepare_content("application/json") + http.write_json(menu) +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/admin/index.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/admin/index.luac new file mode 100644 index 000000000000..45abfc744ff6 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/admin/index.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/admin/uci.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/admin/uci.lua new file mode 100644 index 000000000000..7aad10d58a28 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/admin/uci.lua @@ -0,0 +1,70 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2010-2019 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. + +module("luci.controller.admin.uci", package.seeall) + +local function ubus_state_to_http(errstr) + local map = { + ["Invalid command"] = 400, + ["Invalid argument"] = 400, + ["Method not found"] = 404, + ["Entry not found"] = 404, + ["No data"] = 204, + ["Permission denied"] = 403, + ["Timeout"] = 504, + ["Not supported"] = 500, + ["Unknown error"] = 500, + ["Connection failed"] = 503 + } + + local code = map[errstr] or 200 + local msg = errstr or "OK" + + luci.http.status(code, msg) + + if code ~= 204 then + luci.http.prepare_content("text/plain") + luci.http.write(msg) + end +end + +function action_apply_rollback() + local uci = require "luci.model.uci" + local token, errstr = uci:apply(true) + if token then + luci.http.prepare_content("application/json") + luci.http.write_json({ token = token }) + else + ubus_state_to_http(errstr) + end +end + +function action_apply_unchecked() + local uci = require "luci.model.uci" + local _, errstr = uci:apply(false) + ubus_state_to_http(errstr) +end + +function action_confirm() + local uci = require "luci.model.uci" + local token = luci.http.formvalue("token") + local _, errstr = uci:confirm(token) + ubus_state_to_http(errstr) +end + +function action_revert() + local uci = require "luci.model.uci" + local changes = uci:changes() + + -- Collect files to be reverted + local _, errstr, r, tbl + for r, tbl in pairs(changes) do + _, errstr = uci:revert(r) + if errstr then + break + end + end + + ubus_state_to_http(errstr or "OK") +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/admin/uci.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/admin/uci.luac new file mode 100644 index 000000000000..2e63ef183264 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/admin/uci.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/hwnat.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/hwnat.lua new file mode 100755 index 000000000000..a860f40b9b2c --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/hwnat.lua @@ -0,0 +1,54 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2008 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. +local luci = {} +luci.util = require "luci.util" +luci.http = require "luci.http" + +module("luci.controller.hwnat", package.seeall) +function read_pipe(pipe) + local fp = io.popen(pipe) + local txt = fp:read("*a") + fp:close() + return txt +end + +function index() + if not (nixio.fs.access("/etc/config/hwnat") or nixio.fs.access("/etc/config/hwnat")) then + return + end + + entry({"admin", "mtk", "hwnat"}, cbi("hwnat"), _("NAT Accelerate")) + entry({"admin", "mtk", "hwnat_binding_status"}, call("hwnat_binding_status"), nil).leaf = true +end + +function hwnat_binding_status() + local result = luci.util.execi("hwnat -g") + local data = {} + local t = {} + local proto_num = {} + local i = 0 + + luci.http.prepare_content("application/json") + + if not result then + luci.http.write('[]') + return + end + + for line in result do + if i ~= 0 and line:match(":") then + t = line:split(" ") + proto_num = t[1]:split("=") + data[#data+1] = { + type_ = proto_num[1], + foe_entry = proto_num[2], + src_info = t[3], + new_info = t[5] + } + end + i = i + 1 + end + + luci.http.write_json(data) +end \ No newline at end of file diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/hwnat.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/hwnat.luac new file mode 100644 index 000000000000..96cde61a2a4c Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/hwnat.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/ipsec.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/ipsec.lua new file mode 100755 index 000000000000..e44ef5344910 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/ipsec.lua @@ -0,0 +1,72 @@ +local luci = {} +luci.util = require "luci.util" +luci.http = require "luci.http" +local uci = require "luci.model.uci" + +module("luci.controller.ipsec", package.seeall) + +function index() + entry({"admin", "network", "ipsec"}, cbi("ipsec"), _("IP Security")) + entry({"admin", "network", "ipsec", "vpn_status"}, call("ipsec_vpn_status"), nil).leaf = true + entry({"admin", "network", "ipsec", "vpn_connect"}, call("ipsec_vpn_connect"), nil).leaf = true + entry({"admin", "network", "ipsec", "vpn_disconnect"}, call("ipsec_vpn_disconnect"), nil).leaf = true +end + +function ipsec_vpn_status() + local handle = io.popen(" ipsec status 2>/dev/null") + local result = handle:read("*all") + handle:close() + local obj ={} + + luci.http.prepare_content("application/json") + if result == "" then + obj.status = "Disconnected" + obj.msg = "Disconnected/Command not found" + luci.http.write_json(obj) + return + end + + a = string.match(result, "(%d+) up") + if (tonumber(a) == 0) then + b = string.match(result, "(%d) connecting") + if (tonumber(b) == 0) then + obj.status = "Disconnected" + obj.msg = "Disconnected" + luci.http.write_json(obj) + return + end + obj.status = "Connected" + obj.msg = b.." connecting" + luci.http.write_json(obj) + return + end + for line in result:gmatch("([^\n]*)\n?") do + if (string.find(line, "ESTABLISHED") or string.find(line, "DELETING")) then + obj.status = "Connected" + obj.msg = string.match(line, ": (.*),") + luci.http.write_json(obj) + end + end +end + +function ipsec_vpn_connect() + local l_gw_name = "" + local curs = uci.cursor() + curs:foreach("ipsec", "remote", function(s) l_gw_name = s[".name"] end) + l_subnet = curs:get("ipsec", "TUNNEL", "local_subnet") + l_wan = curs:get("network","wan" ,"device") + + luci.util.execi("iptables -t nat -I POSTROUTING -o "..l_wan.." -s "..l_subnet.." -j ACCEPT") + luci.util.execi("ipsec down "..l_gw_name.."-TUNNEL") + luci.util.execi("ipsec up "..l_gw_name.."-TUNNEL") + ipsec_vpn_status() +end + +function ipsec_vpn_disconnect() + local l_gw_name = "" + local curs = uci.cursor() + curs:foreach("ipsec", "remote", function(s) l_gw_name = s[".name"] end) + + luci.util.execi("ipsec down "..l_gw_name.."-TUNNEL") + ipsec_vpn_status() +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/ipsec.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/ipsec.luac new file mode 100644 index 000000000000..0fe99ce6ab62 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/ipsec.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/mtkwifi.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/mtkwifi.lua new file mode 100755 index 000000000000..6048eecd6788 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/mtkwifi.lua @@ -0,0 +1,2738 @@ +-- This module is a demo to configure MTK' proprietary WiFi driver. +-- Basic idea is to bypass uci and edit wireless profile (mt76xx.dat) directly. +-- LuCI's WiFi configuration is more logical and elegent, but it's quite tricky to +-- translate uci into MTK's WiFi profile (like we did in "uci2dat"). +-- And you will get your hands dirty. +-- +-- Hua Shao + +package.path = '/lib/wifi/?.lua;'..package.path +module("luci.controller.mtkwifi", package.seeall) +local onboardingType = 0; +local ioctl_help = require "ioctl_helper" +local map_help +if pcall(require, "map_helper") then + map_help = require "map_helper" +end +local http = require("luci.http") +local mtkwifi = require("mtkwifi") + +local logDisable = 1 +function debug_write(...) + -- luci.http.write(...) + if logDisable == 1 then + return + end + local syslog_msg = ""; + local ff = io.open("/tmp/dbgmsg", "a") + local nargs = select('#',...) + + for n=1, nargs do + local v = select(n,...) + if (type(v) == "string" or type(v) == "number") then + ff:write(v.." ") + syslog_msg = syslog_msg..v.." "; + elseif (type(v) == "boolean") then + if v then + ff:write("true ") + syslog_msg = syslog_msg.."true "; + else + ff:write("false ") + syslog_msg = syslog_msg.."false "; + end + elseif (type(v) == "nil") then + ff:write("nil ") + syslog_msg = syslog_msg.."nil "; + else + ff:write(" ") + syslog_msg = syslog_msg.." "; + end + end + ff:write("\n") + ff:close() + nixio.syslog("debug", syslog_msg) +end + +function index() + -- if not nixio.fs.access("/etc/wireless") then + -- return + -- end + + entry({"admin", "mtk"}, firstchild(), _("MTK"), 80) + entry({"admin", "mtk", "test"}, call("test")) + entry({"admin", "mtk", "wifi"}, template("admin_mtk/mtk_wifi_overview"), _("WiFi configuration"), 1) + entry({"admin", "mtk", "wifi", "chip_cfg_view"}, template("admin_mtk/mtk_wifi_chip_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "chip_cfg"}, call("chip_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "dev_cfg_view"}, template("admin_mtk/mtk_wifi_dev_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "dev_cfg"}, call("dev_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "dev_cfg_raw"}, call("dev_cfg_raw")).leaf = true + entry({"admin", "mtk", "wifi", "vif_cfg_view"}, template("admin_mtk/mtk_wifi_vif_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "vif_cfg"}, call("vif_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "vif_add_view"}, template("admin_mtk/mtk_wifi_vif_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "vif_add"}, call("vif_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "vif_del"}, call("vif_del")).leaf = true + entry({"admin", "mtk", "wifi", "vif_disable"}, call("vif_disable")).leaf = true + entry({"admin", "mtk", "wifi", "vif_enable"}, call("vif_enable")).leaf = true + entry({"admin", "mtk", "wifi", "get_station_list"}, call("get_station_list")) + entry({"admin", "mtk", "wifi", "get_country_region_list"}, call("get_country_region_list")).leaf = true + entry({"admin", "mtk", "wifi", "get_channel_list"}, call("get_channel_list")) + entry({"admin", "mtk", "wifi", "get_HT_ext_channel_list"}, call("get_HT_ext_channel_list")) + entry({"admin", "mtk", "wifi", "get_5G_2nd_80Mhz_channel_list"}, call("get_5G_2nd_80Mhz_channel_list")) + entry({"admin", "mtk", "wifi", "reset"}, call("reset_wifi")).leaf = true + entry({"admin", "mtk", "wifi", "reload"}, call("reload_wifi")).leaf = true + entry({"admin", "mtk", "wifi", "get_raw_profile"}, call("get_raw_profile")) + entry({"admin", "mtk", "wifi", "apcli_cfg_view"}, template("admin_mtk/mtk_wifi_apcli")).leaf = true + entry({"admin", "mtk", "wifi", "apcli_cfg"}, call("apcli_cfg")).leaf = true + entry({"admin", "mtk", "wifi", "apcli_disconnect"}, call("apcli_disconnect")).leaf = true + entry({"admin", "mtk", "wifi", "apcli_connect"}, call("apcli_connect")).leaf = true + entry({"admin", "mtk", "netmode", "net_cfg"}, call("net_cfg")) + entry({"admin", "mtk", "console"}, template("admin_mtk/mtk_web_console"), _("Web Console"), 4) + entry({"admin", "mtk", "webcmd"}, call("webcmd")) + -- entry({"admin", "mtk", "man"}, template("admin_mtk/mtk_wifi_man"), _("M.A.N"), 3) + -- entry({"admin", "mtk", "man", "cfg"}, call("man_cfg")) + entry({"admin", "mtk", "wifi", "get_wps_info"}, call("get_WPS_Info")).leaf = true + entry({"admin", "mtk", "wifi", "get_wifi_pin"}, call("get_wifi_pin")).leaf = true + entry({"admin", "mtk", "wifi", "set_wifi_gen_pin"}, call("set_wifi_gen_pin")).leaf = true + entry({"admin", "mtk", "wifi", "set_wifi_wps_oob"}, call("set_wifi_wps_oob")).leaf = true + entry({"admin", "mtk", "wifi", "set_wifi_do_wps"}, call("set_wifi_do_wps")).leaf = true + entry({"admin", "mtk", "wifi", "get_wps_security"}, call("get_wps_security")).leaf = true + entry({"admin", "mtk", "wifi", "apcli_get_wps_status"}, call("apcli_get_wps_status")).leaf = true; + entry({"admin", "mtk", "wifi", "apcli_do_enr_pin_wps"}, call("apcli_do_enr_pin_wps")).leaf = true; + entry({"admin", "mtk", "wifi", "apcli_do_enr_pbc_wps"}, call("apcli_do_enr_pbc_wps")).leaf = true; + entry({"admin", "mtk", "wifi", "apcli_cancel_wps"}, call("apcli_cancel_wps")).leaf = true; + entry({"admin", "mtk", "wifi", "apcli_wps_gen_pincode"}, call("apcli_wps_gen_pincode")).leaf = true; + entry({"admin", "mtk", "wifi", "apcli_wps_get_pincode"}, call("apcli_wps_get_pincode")).leaf = true; + entry({"admin", "mtk", "wifi", "apcli_scan"}, call("apcli_scan")).leaf = true; + entry({"admin", "mtk", "wifi", "sta_info"}, call("sta_info")).leaf = true; + entry({"admin", "mtk", "wifi", "get_apcli_conn_info"}, call("get_apcli_conn_info")).leaf = true; + entry({"admin", "mtk", "wifi", "apply_power_boost_settings"}, call("apply_power_boost_settings")).leaf = true; + entry({"admin", "mtk", "wifi", "apply_reboot"}, template("admin_mtk/mtk_wifi_apply_reboot")).leaf = true; + entry({"admin", "mtk", "wifi", "reboot"}, call("exec_reboot")).leaf = true; + entry({"admin", "mtk", "wifi", "get_bssid_num"}, call("get_bssid_num")).leaf = true; + entry({"admin", "mtk", "wifi", "loading"}, template("admin_mtk/mtk_wifi_loading")).leaf = true; + entry({"admin", "mtk", "wifi", "get_apply_status"}, call("get_apply_status")).leaf = true; + entry({"admin", "mtk", "wifi", "reset_to_defaults"}, call("reset_to_defaults")).leaf = true; + local mtkwifi = require("mtkwifi") + -- local profiles = mtkwifi.search_dev_and_profile() + -- for devname,profile in pairs(profiles) do + -- local cfgs = mtkwifi.load_profile(profile) + -- if cfgs["VOW_Airtime_Fairness_En"] then + -- entry({"admin", "mtk", "vow"}, template("admin_mtk/mtk_vow"), _("VoW / ATF / ATC"), 4) + -- break + -- end + -- end + + -- Define map_help again here as same defination at top does not come under scope of luci library. + local map_help + if pcall(require, "map_helper") then + map_help = require "map_helper" + end + if map_help then + entry({"admin", "mtk", "multi_ap", "reset_to_default_easymesh"}, call("reset_to_default_easymesh")).leaf = true; + entry({"admin", "mtk", "multi_ap"}, template("admin_mtk/mtk_wifi_multi_ap"), _("EasyMesh"), 5); + entry({"admin", "mtk", "multi_ap", "map_cfg"}, call("map_cfg")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_device_role"}, call("get_device_role")).leaf = true; + entry({"admin", "mtk", "multi_ap", "trigger_mandate_steering_on_agent"}, call("trigger_mandate_steering_on_agent")).leaf = true; + entry({"admin", "mtk", "multi_ap", "trigger_back_haul_steering_on_agent"}, call("trigger_back_haul_steering_on_agent")).leaf = true; + entry({"admin", "mtk", "multi_ap", "trigger_wps_fh_agent"}, call("trigger_wps_fh_agent")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_runtime_topology"}, template("admin_mtk/mtk_wifi_map_runtime_topology")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_runtime_topology"}, call("get_runtime_topology")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_data_element"}, template("admin_mtk/mtk_wifi_map_data_element")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_channel_scan_result"}, template("admin_mtk/mtk_wifi_map_channel_scan_result")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_channel_planning_score"}, template("admin_mtk/mtk_wifi_map_channel_planning_score")).leaf = true; + entry({"admin", "mtk", "multi_ap", "trigger_multi_ap_on_boarding"}, call("trigger_multi_ap_on_boarding")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_client_capabilities"}, template("admin_mtk/mtk_wifi_map_client_capabilities")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_client_capabilities"}, call("get_client_capabilities")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_ap_capabilities"}, template("admin_mtk/mtk_wifi_map_ap_capabilities")).leaf = true; + entry({"admin", "mtk", "multi_ap", "trigger_uplink_ap_selection"}, call("trigger_uplink_ap_selection")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_bh_connection_status"}, call("get_bh_connection_status")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_sta_steering_progress"}, call("get_sta_steering_progress")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_al_mac"}, call("get_al_mac")).leaf = true; + entry({"admin", "mtk", "multi_ap", "apply_wifi_bh_priority"}, call("apply_wifi_bh_priority")).leaf = true; + entry({"admin", "mtk", "multi_ap", "apply_ap_steer_rssi_th"}, call("apply_ap_steer_rssi_th")).leaf = true; + entry({"admin", "mtk", "multi_ap", "apply_channel_utilization_th"}, call("apply_channel_utilization_th")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_sta_bh_interface"}, call("get_sta_bh_interface")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_ap_bh_inf_list"}, call("get_ap_bh_inf_list")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_ap_fh_inf_list"}, call("get_ap_fh_inf_list")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_fh_status_bss"}, template("admin_mtk/mtk_wifi_map_bssinfo")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_bh_link_metrics_ctrler"}, template("admin_mtk/mtk_wifi_map_bh_link_metrics")).leaf = true; + entry({"admin", "mtk", "multi_ap", "easymesh_bss_config_renew"}, template("admin_mtk/mtk_wifi_map_bss_cfg_renew")).leaf = true; + entry({"admin", "mtk", "multi_ap", "easymesh_bss_cfg"}, call("easymesh_bss_cfg")).leaf = true; + entry({"admin", "mtk", "multi_ap", "validate_add_easymesh_bss_req"}, call("validate_add_easymesh_bss_req")).leaf = true; + entry({"admin", "mtk", "multi_ap", "remove_easymesh_bss_cfg_req"}, call("remove_easymesh_bss_cfg_req")).leaf = true; + entry({"admin", "mtk", "multi_ap", "apply_easymesh_bss_cfg"}, call("apply_easymesh_bss_cfg")).leaf = true; + entry({"admin", "mtk", "multi_ap", "apply_force_ch_switch"}, call("apply_force_ch_switch")).leaf = true; + entry({"admin", "mtk", "multi_ap", "apply_user_preferred_channel"}, call("apply_user_preferred_channel")).leaf = true; + entry({"admin", "mtk", "multi_ap", "trigger_channel_planning_r2"}, call("trigger_channel_planning_r2")).leaf = true; + entry({"admin", "mtk", "multi_ap", "trigger_de_dump"}, call("trigger_de_dump")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_data_element"}, call("get_data_element")).leaf = true; + entry({"admin", "mtk", "multi_ap", "trigger_channel_scan"}, call("trigger_channel_scan")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_channel_stats"}, call("get_channel_stats")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_channel_planning_score"}, call("get_channel_planning_score")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_user_preferred_channel"}, call("get_user_preferred_channel")).leaf = true; + entry({"admin", "mtk", "multi_ap", "get_sp_rule_list"}, call("get_sp_rule_list")).leaf = true; + entry({"admin", "mtk", "multi_ap", "del_sp_rule"}, call("del_sp_rule")).leaf = true; + entry({"admin", "mtk", "multi_ap", "sp_rule_reorder"}, call("sp_rule_reorder")).leaf = true; + entry({"admin", "mtk", "multi_ap", "sp_rule_move"}, call("sp_rule_move")).leaf = true; + entry({"admin", "mtk", "multi_ap", "add_sp_rule"}, call("sp_rule_add")).leaf = true; + entry({"admin", "mtk", "multi_ap", "sp_config_done"}, call("sp_config_done")).leaf = true; + entry({"admin", "mtk", "multi_ap", "submit_dpp_uri"}, call("submit_dpp_uri")).leaf = true; + entry({"admin", "mtk", "multi_ap", "display_bootstrapping_uri"}, template("admin_mtk/mtk_wifi_map_display_bootstrapping_uri")).leaf = true; + entry({"admin", "mtk", "multi_ap", "start_dpp_onboarding"}, call("start_dpp_onboarding")).leaf = true; + entry({"admin", "mtk", "multi_ap", "generate_dpp_uri"}, call("generate_dpp_uri")).leaf = true; + entry({"admin", "mtk", "multi_ap", "retrive_dpp_uri"}, call("retrive_dpp_uri")).leaf = true; + end +end + +function test() + http.write_json(http.formvalue()) +end + +function exec_reboot() + os.execute("rm -f /tmp/mtk/wifi/reboot_required >/dev/null 2>&1") + os.execute("sync >/dev/null 2>&1") + os.execute("reboot >/dev/null 2>&1") +end + +function get_apply_status() + local ret = {} + + if mtkwifi.is_child_active() then + ret["status"] = "ON_PROGRESS" + elseif mtkwifi.exists("/tmp/mtk/wifi/reboot_required") then + -- If the "wifi restart" command can not re-install the driver; then, it will create + -- "/tmp/mtk/wifi/reboot_required" file to indicate LuCI that the settings will be applied + -- only after reboot of the device. + -- Redirect "Reboot Device" web-page to get consent from the user to reboot the device. + ret["status"] = "REBOOT" + else + ret["status"] = "DONE" + end + http.write_json(ret) +end + +function __mtkwifi_save_profile(cfgs, path, isProfileSettingsAppliedToDriver) + -- Create the applied settings backup file before saving the new profile settings only if it does not exist. + if not mtkwifi.exists(mtkwifi.__profile_applied_settings_path(path)) then + os.execute("cp -f "..path.." "..mtkwifi.__profile_applied_settings_path(path)) + end + if isProfileSettingsAppliedToDriver then + -- It means the some context based profile settings to be saved in DAT file is already applied to the driver. + -- Find the profile settings which are not applied to the driver before saving the new profile settings + local diff = mtkwifi.diff_profile(path) + mtkwifi.save_profile(cfgs, path) + -- If there are any settings which are not applied to the driver, then do NOT copy and WebUI will display the "need reload to apply changes" message + -- Otherwise, copy the new profile settings and WebUI will NOT display the "need reload to apply changes" message + if next(diff) == nil then + os.execute("cp -f "..path.." "..mtkwifi.__profile_applied_settings_path(path)) + end + else + mtkwifi.save_profile(cfgs, path) + end +end + +local __mtkwifi_reload = function (devname) + local wifi_restart = false + local wifi_reload = false + local profiles = mtkwifi.search_dev_and_profile() + + for dev,profile in pairs(profiles) do + if not devname or devname == dev then + local diff = mtkwifi.diff_profile(profile) + local diff_easy = mtkwifi.diff_profile(mtkwifi.__write_easymesh_profile_path(), mtkwifi.__profile_applied_settings_path(mtkwifi.__write_easymesh_profile_path())) + if not next(diff) and not next(diff_easy) then return end + __process_settings_before_apply(dev, profile, diff) + + if diff.BssidNum or diff.WHNAT or diff.E2pAccessMode or diff.HT_RxStream or diff.HT_TxStream or diff.HE_LDPC or diff.WdsEnable then + -- Addition or deletion of a vif requires re-installation of the driver. + -- Change in WHNAT setting also requires re-installation of the driver. + -- Driver will be re-installed by "wifi restart" command. + wifi_restart = true + else + wifi_reload = true + end + + end + end + + if wifi_restart then + os.execute("wifi restart "..(devname or "")) + debug_write("wifi restart "..(devname or "")) + elseif wifi_reload then + os.execute("wifi reload "..(devname or "")) + debug_write("wifi reload "..(devname or "")) + end + + for dev,profile in pairs(profiles) do + if not devname or devname == dev then + -- keep a backup for this commit + -- it will be used in mtkwifi.diff_profile() + os.execute("cp -f "..profile.." "..mtkwifi.__profile_applied_settings_path(profile)) + debug_write("cp -f "..profile.." "..mtkwifi.__profile_applied_settings_path(profile)) + end + end + + if map_help then + local easymesh_applied_path = mtkwifi.__profile_applied_settings_path(mtkwifi.__read_easymesh_profile_path()) + os.execute("cp -f "..mtkwifi.__read_easymesh_profile_path().." "..easymesh_applied_path) + end +end + +function __process_settings_before_apply(devname, profile, diff) + local devs = mtkwifi.get_all_devs() + local cfgs = mtkwifi.load_profile(profile) + __apply_wifi_wpsconf(devs, devname, cfgs, diff) +end + +function chip_cfg(devname) + local profiles = mtkwifi.search_dev_and_profile() + assert(profiles[devname]) + local cfgs = mtkwifi.load_profile(profiles[devname]) + local devs = mtkwifi.get_all_devs() + local dbdc_cfgs = {} + local dev = {} + dev = devs and devs[devname] + + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + nixio.syslog("err", "chip_cfg, invalid value type for "..k..","..type(v)) + elseif string.byte(k) == string.byte("_") then + nixio.syslog("err", "chip_cfg, special: "..k.."="..v) + else + if dev.dbdc == true then + dbdc_cfgs[k] = v or "" + else + cfgs[k] = v or "" + end + end + end + + -- VOW + -- ATC should actually be scattered into each SSID, but I'm just lazy. + if cfgs.VOW_Airtime_Fairness_En then + for i = 1,tonumber(cfgs.BssidNum) do + __atc_tp = http.formvalue("__atc_vif"..i.."_tp") or "0" + __atc_min_tp = http.formvalue("__atc_vif"..i.."_min_tp") or "0" + __atc_max_tp = http.formvalue("__atc_vif"..i.."_max_tp") or "0" + __atc_at = http.formvalue("__atc_vif"..i.."_at") or "0" + __atc_min_at = http.formvalue("__atc_vif"..i.."_min_at") or "0" + __atc_max_at = http.formvalue("__atc_vif"..i.."_max_at") or "0" + + nixio.syslog("info", "ATC.__atc_tp ="..i..__atc_tp ); + nixio.syslog("info", "ATC.__atc_min_tp ="..i..__atc_min_tp ); + nixio.syslog("info", "ATC.__atc_max_tp ="..i..__atc_max_tp ); + nixio.syslog("info", "ATC.__atc_at ="..i..__atc_at ); + nixio.syslog("info", "ATC.__atc_min_at ="..i..__atc_min_at ); + nixio.syslog("info", "ATC.__atc_max_at ="..i..__atc_max_at ); + + dbdc_cfgs.VOW_Rate_Ctrl_En = mtkwifi.token_set(cfgs.VOW_Rate_Ctrl_En, i, __atc_tp) + dbdc_cfgs.VOW_Group_Min_Rate = mtkwifi.token_set(cfgs.VOW_Group_Min_Rate, i, __atc_min_tp) + dbdc_cfgs.VOW_Group_Max_Rate = mtkwifi.token_set(cfgs.VOW_Group_Max_Rate, i, __atc_max_tp) + + dbdc_cfgs.VOW_Airtime_Ctrl_En = mtkwifi.token_set(cfgs.VOW_Airtime_Ctrl_En, i, __atc_at) + dbdc_cfgs.VOW_Group_Min_Ratio = mtkwifi.token_set(cfgs.VOW_Group_Min_Ratio, i, __atc_min_at) + dbdc_cfgs.VOW_Group_Max_Ratiio = mtkwifi.token_set(cfgs.VOW_Group_Max_Ratio, i, __atc_max_at) + + cfgs.VOW_Rate_Ctrl_En = mtkwifi.token_set(cfgs.VOW_Rate_Ctrl_En, i, __atc_tp) + cfgs.VOW_Group_Min_Rate = mtkwifi.token_set(cfgs.VOW_Group_Min_Rate, i, __atc_min_tp) + cfgs.VOW_Group_Max_Rate = mtkwifi.token_set(cfgs.VOW_Group_Max_Rate, i, __atc_max_tp) + + cfgs.VOW_Airtime_Ctrl_En = mtkwifi.token_set(cfgs.VOW_Airtime_Ctrl_En, i, __atc_at) + cfgs.VOW_Group_Min_Ratio = mtkwifi.token_set(cfgs.VOW_Group_Min_Ratio, i, __atc_min_at) + cfgs.VOW_Group_Max_Ratio = mtkwifi.token_set(cfgs.VOW_Group_Max_Ratio, i, __atc_max_at) + + end + + dbdc_cfgs.VOW_RX_En = http.formvalue("VOW_RX_En") or "0" + cfgs.VOW_RX_En = http.formvalue("VOW_RX_En") or "0" + end + + if dev.dbdc == true then + for devname, profile in pairs(profiles) do + __mtkwifi_save_profile(dbdc_cfgs, profile, false) + end + else + __mtkwifi_save_profile(cfgs, profiles[devname], false) + end + + if http.formvalue("__apply") then + mtkwifi.__run_in_child_env(__mtkwifi_reload, devname) + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "mtk", "wifi", "chip_cfg_view",devname) + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",url_to_visit_after_reload)) + else + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "chip_cfg_view",devname)) + end + +end + +function dev_cfg(devname) + local profiles = mtkwifi.search_dev_and_profile() + assert(profiles[devname]) + local cfgs = mtkwifi.load_profile(profiles[devname]) + + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + nixio.syslog("err", "dev_cfg, invalid value type for "..k..","..type(v)) + elseif string.byte(k) == string.byte("_") then + nixio.syslog("err", "dev_cfg, special: "..k.."="..v) + else + cfgs[k] = v or "" + end + end + + if cfgs.Channel == "0" then -- Auto Channel Select + cfgs.AutoChannelSelect = "3" + else + cfgs.AutoChannelSelect = "0" + end + + if http.formvalue("__bw") == "20" then + cfgs.HT_BW = 0 + cfgs.VHT_BW = 0 + elseif http.formvalue("__bw") == "40" then + cfgs.HT_BW = 1 + cfgs.VHT_BW = 0 + cfgs.HT_BSSCoexistence = 0 + elseif http.formvalue("__bw") == "60" then + cfgs.HT_BW = 1 + cfgs.VHT_BW = 0 + cfgs.HT_BSSCoexistence = 1 + elseif http.formvalue("__bw") == "80" then + cfgs.HT_BW = 1 + cfgs.VHT_BW = 1 + elseif http.formvalue("__bw") == "160" then + cfgs.HT_BW = 1 + cfgs.VHT_BW = 2 + elseif http.formvalue("__bw") == "161" then + cfgs.HT_BW = 1 + cfgs.VHT_BW = 3 + cfgs.VHT_Sec80_Channel = http.formvalue("VHT_Sec80_Channel") or "" + end + + if mtkwifi.band(string.split(cfgs.WirelessMode,";")[1]) == "5G" or mtkwifi.band(cfgs.WirelessMode) == "6G" then + cfgs.CountryRegionABand = http.formvalue("__cr"); + else + cfgs.CountryRegion = http.formvalue("__cr"); + end + + if http.formvalue("TxPower") then + local txpower = tonumber(http.formvalue("TxPower")) + if txpower < 100 then + cfgs.PERCENTAGEenable=1 + else + cfgs.PERCENTAGEenable=0 + end + end + + local IndividualTWTSupport = tonumber(http.formvalue("IndividualTWTSupport")) + if IndividualTWTSupport == 0 then + cfgs.TWTResponder=0 + cfgs.TWTRequired=0 + elseif IndividualTWTSupport == 1 then + cfgs.TWTResponder=1 + cfgs.TWTRequired=0 + else + cfgs.TWTResponder=1 + cfgs.TWTRequired=1 + end + + local mimo = http.formvalue("__mimo") + if mimo == "0" then + cfgs.ETxBfEnCond=1 + cfgs.MUTxRxEnable=0 + cfgs.ITxBfEn=0 + elseif mimo == "1" then + cfgs.ETxBfEnCond=0 + cfgs.MUTxRxEnable=0 + cfgs.ITxBfEn=1 + elseif mimo == "2" then + cfgs.ETxBfEnCond=1 + cfgs.MUTxRxEnable=0 + cfgs.ITxBfEn=1 + elseif mimo == "3" then + cfgs.ETxBfEnCond=1 + if tonumber(cfgs.ApCliEnable) == 1 then + cfgs.MUTxRxEnable=3 + else + cfgs.MUTxRxEnable=1 + end + cfgs.ITxBfEn=0 + elseif mimo == "4" then + cfgs.ETxBfEnCond=1 + if tonumber(cfgs.ApCliEnable) == 1 then + cfgs.MUTxRxEnable=3 + else + cfgs.MUTxRxEnable=1 + end + cfgs.ITxBfEn=1 + else + cfgs.ETxBfEnCond=0 + cfgs.MUTxRxEnable=0 + cfgs.ITxBfEn=0 + end + +-- if cfgs.ApCliEnable == "1" then +-- cfgs.Channel = http.formvalue("__apcli_channel") +-- end + + -- WDS + -- http.write_json(http.formvalue()) + __mtkwifi_save_profile(cfgs, profiles[devname], false) + + if http.formvalue("__apply") then + mtkwifi.__run_in_child_env(__mtkwifi_reload, devname) + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "mtk", "wifi", "dev_cfg_view",devname) + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",url_to_visit_after_reload)) + else + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "dev_cfg_view",devname)) + end +end + +function dev_cfg_raw(devname) + -- http.write_json(http.formvalue()) + local profiles = mtkwifi.search_dev_and_profile() + assert(profiles[devname]) + + local raw = http.formvalue("raw") + raw = string.gsub(raw, "\r\n", "\n") + local cfgs = mtkwifi.load_profile(nil, raw) + __mtkwifi_save_profile(cfgs, profiles[devname], false) + + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "dev_cfg_view", devname)) +end + +function __delete_mbss_para(cfgs, vif_idx) + debug_write(vif_idx) + cfgs["WPAPSK"..vif_idx]="" + cfgs["Key1Type"]=mtkwifi.token_set(cfgs["Key1Type"],vif_idx,"") + cfgs["Key2Type"]=mtkwifi.token_set(cfgs["Key2Type"],vif_idx,"") + cfgs["Key3Type"]=mtkwifi.token_set(cfgs["Key3Type"],vif_idx,"") + cfgs["Key4Type"]=mtkwifi.token_set(cfgs["Key4Type"],vif_idx,"") + cfgs["RADIUS_Server"]=mtkwifi.token_set(cfgs["RADIUS_Server"],vif_idx,"") + cfgs["RADIUS_Port"]=mtkwifi.token_set(cfgs["RADIUS_Port"],vif_idx,"") + cfgs["RADIUS_Key"..vif_idx]="" + cfgs["DefaultKeyID"]=mtkwifi.token_set(cfgs["DefaultKeyID"],vif_idx,"") + cfgs["IEEE8021X"]=mtkwifi.token_set(cfgs["IEEE8021X"],vif_idx,"") + cfgs["WscConfMode"]=mtkwifi.token_set(cfgs["WscConfMode"],vif_idx,"") + cfgs["PreAuth"]=mtkwifi.token_set(cfgs["PreAuth"],vif_idx,"") + cfgs["HT_STBC"] = mtkwifi.token_set(cfgs["HT_STBC"],vif_idx,"") + cfgs["HT_LDPC"] = mtkwifi.token_set(cfgs["HT_LDPC"],vif_idx,"") + cfgs["VHT_STBC"] = mtkwifi.token_set(cfgs["VHT_STBC"],vif_idx,"") + cfgs["VHT_LDPC"] = mtkwifi.token_set(cfgs["VHT_LDPC"],vif_idx,"") + cfgs["HideSSID"]=mtkwifi.token_set(cfgs["HideSSID"],vif_idx,"") + cfgs["NoForwarding"]=mtkwifi.token_set(cfgs["NoForwarding"],vif_idx,"") + cfgs["WmmCapable"]=mtkwifi.token_set(cfgs["WmmCapable"],vif_idx,"") + cfgs["TxRate"]=mtkwifi.token_set(cfgs["TxRate"],vif_idx,"") + cfgs["RekeyInterval"]=mtkwifi.token_set(cfgs["RekeyInterval"],vif_idx,"") + cfgs["AuthMode"]=mtkwifi.token_set(cfgs["AuthMode"],vif_idx,"") + cfgs["EncrypType"]=mtkwifi.token_set(cfgs["EncrypType"],vif_idx,"") + cfgs["session_timeout_interval"]=mtkwifi.token_set(cfgs["session_timeout_interval"],vif_idx,"") + cfgs["WscModeOption"]=mtkwifi.token_set(cfgs["WscModeOption"],vif_idx,"") + cfgs["RekeyMethod"]=mtkwifi.token_set(cfgs["RekeyMethod"],vif_idx,"") + cfgs["PMFMFPC"] = mtkwifi.token_set(cfgs["PMFMFPC"],vif_idx,"") + cfgs["PMFMFPR"] = mtkwifi.token_set(cfgs["PMFMFPR"],vif_idx,"") + cfgs["PMFSHA256"] = mtkwifi.token_set(cfgs["PMFSHA256"],vif_idx,"") + cfgs["PMKCachePeriod"] = mtkwifi.token_set(cfgs["PMKCachePeriod"],vif_idx,"") + cfgs["Wapiifname"] = mtkwifi.token_set(cfgs["Wapiifname"],vif_idx,"") + cfgs["RRMEnable"] = mtkwifi.token_set(cfgs["RRMEnable"],vif_idx,"") + cfgs["DLSCapable"] = mtkwifi.token_set(cfgs["DLSCapable"],vif_idx,"") + cfgs["APSDCapable"] = mtkwifi.token_set(cfgs["APSDCapable"],vif_idx,"") + cfgs["FragThreshold"] = mtkwifi.token_set(cfgs["FragThreshold"],vif_idx,"") + cfgs["RTSThreshold"] = mtkwifi.token_set(cfgs["RTSThreshold"],vif_idx,"") + cfgs["VHT_SGI"] = mtkwifi.token_set(cfgs["VHT_SGI"],vif_idx,"") + cfgs["VHT_BW_SIGNAL"] = mtkwifi.token_set(cfgs["VHT_BW_SIGNAL"],vif_idx,"") + cfgs["HT_PROTECT"] = mtkwifi.token_set(cfgs["HT_PROTECT"],vif_idx,"") + cfgs["HT_GI"] = mtkwifi.token_set(cfgs["HT_GI"],vif_idx,"") + cfgs["HT_OpMode"] = mtkwifi.token_set(cfgs["HT_OpMode"],vif_idx,"") + cfgs["HT_TxStream"] = mtkwifi.token_set(cfgs["HT_TxStream"],vif_idx,"") + cfgs["HT_RxStream"] = mtkwifi.token_set(cfgs["HT_RxStream"],vif_idx,"") + cfgs["HT_AMSDU"] = mtkwifi.token_set(cfgs["HT_AMSDU"],vif_idx,"") + cfgs["HT_AutoBA"] = mtkwifi.token_set(cfgs["HT_AutoBA"],vif_idx,"") + cfgs["IgmpSnEnable"] = mtkwifi.token_set(cfgs["IgmpSnEnable"],vif_idx,"") + cfgs["WirelessMode"] = mtkwifi.token_set(cfgs["WirelessMode"],vif_idx,"") + cfgs["WdsEnable"] = mtkwifi.token_set(cfgs["WdsEnable"],vif_idx,"") + cfgs["MuOfdmaDlEnable"] = mtkwifi.token_set(cfgs["MuOfdmaDlEnable"],vif_idx,"") + cfgs["MuOfdmaUlEnable"] = mtkwifi.token_set(cfgs["MuOfdmaUlEnable"],vif_idx,"") + cfgs["MuMimoDlEnable"] = mtkwifi.token_set(cfgs["MuMimoDlEnable"],vif_idx,"") + cfgs["MuMimoUlEnable"] = mtkwifi.token_set(cfgs["MuMimoUlEnable"],vif_idx,"") + +end + +function vif_del(dev, vif) + debug_write("vif_del("..dev..vif..")") + local devname,vifname = dev, vif + debug_write("devname="..devname) + debug_write("vifname="..vifname) + local devs = mtkwifi.get_all_devs() + local idx = devs[devname]["vifs"][vifname].vifidx -- or tonumber(string.match(vifname, "%d+")) + 1 + debug_write("idx="..idx, devname, vifname) + local profile = devs[devname].profile + assert(profile) + if idx and tonumber(idx) >= 0 then + local cfgs = mtkwifi.load_profile(profile) + __delete_mbss_para(cfgs, idx) + if cfgs then + debug_write("ssid"..idx.."="..cfgs["SSID"..idx].."
") + cfgs["SSID"..idx] = "" + debug_write("ssid"..idx.."="..cfgs["SSID"..idx].."
") + debug_write("wpapsk"..idx.."="..cfgs["WPAPSK"..idx].."
") + cfgs["WPAPSK"..idx] = "" + local ssidlist = {} + local j = 1 + for i = 1,16 do + if cfgs["SSID"..i] ~= "" then + ssidlist[j] = cfgs["SSID"..i] + j = j + 1 + end + end + for i,v in ipairs(ssidlist) do + debug_write("ssidlist"..i.."="..v) + end + debug_write("cfgs.BssidNum="..cfgs.BssidNum.." #ssidlist="..#ssidlist) + assert(tonumber(cfgs.BssidNum) == #ssidlist + 1, "BssidNum="..cfgs.BssidNum.." SSIDlist="..#ssidlist..", BssidNum count does not match with SSIDlist count.") + cfgs.BssidNum = #ssidlist + for i = 1,16 do + if i <= cfgs.BssidNum then + cfgs["SSID"..i] = ssidlist[i] + elseif cfgs["SSID"..i] then + cfgs["SSID"..i] = "" + end + end + + __mtkwifi_save_profile(cfgs, profile, false) + else + debug_write(profile.." cannot be found!") + end + end + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi")) +end + +function vif_disable(iface) + os.execute("ifconfig "..iface.." down") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi")) +end + +function vif_enable(iface) + os.execute("ifconfig "..iface.." up") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi")) +end + + +--[[ +-- security config in mtk wifi is quite complicated! +-- cfgs listed below are attached with vif and combined like "0;0;0;0". They need specicial treatment. + TxRate, WmmCapable, NoForwarding, + HideSSID, IEEE8021X, PreAuth, + AuthMode, EncrypType, RekeyMethod, + RekeyInterval, PMKCachePeriod, + DefaultKeyId, Key{n}Type, HT_EXTCHA, + RADIUS_Server, RADIUS_Port, +]] + +local function conf_wep_keys(cfgs,vifidx) + cfgs.DefaultKeyID = mtkwifi.token_set(cfgs.DefaultKeyID, vifidx, http.formvalue("__DefaultKeyID") or 1) + cfgs["Key1Str"..vifidx] = http.formvalue("Key1Str"..vifidx) + cfgs["Key2Str"..vifidx] = http.formvalue("Key2Str"..vifidx) + cfgs["Key3Str"..vifidx] = http.formvalue("Key3Str"..vifidx) + cfgs["Key4Str"..vifidx] = http.formvalue("Key4Str"..vifidx) + + cfgs["Key1Type"]=mtkwifi.token_set(cfgs["Key1Type"],vifidx, http.formvalue("WEP1Type"..vifidx)) + cfgs["Key2Type"]=mtkwifi.token_set(cfgs["Key2Type"],vifidx, http.formvalue("WEP2Type"..vifidx)) + cfgs["Key3Type"]=mtkwifi.token_set(cfgs["Key3Type"],vifidx, http.formvalue("WEP3Type"..vifidx)) + cfgs["Key4Type"]=mtkwifi.token_set(cfgs["Key4Type"],vifidx, http.formvalue("WEP4Type"..vifidx)) + + return cfgs +end + +local function __security_cfg(cfgs, vif_idx) + debug_write("__security_cfg, before, HideSSID="..tostring(cfgs.HideSSID)) + debug_write("__security_cfg, before, NoForwarding="..tostring(cfgs.NoForwarding)) + debug_write("__security_cfg, before, WmmCapable="..tostring(cfgs.WmmCapable)) + debug_write("__security_cfg, before, TxRate="..tostring(cfgs.TxRate)) + debug_write("__security_cfg, before, RekeyInterval="..tostring(cfgs.RekeyInterval)) + debug_write("__security_cfg, before, AuthMode="..tostring(cfgs.AuthMode)) + debug_write("__security_cfg, before, EncrypType="..tostring(cfgs.EncrypType)) + debug_write("__security_cfg, before, WscModeOption="..tostring(cfgs.WscModeOption)) + debug_write("__security_cfg, before, RekeyMethod="..tostring(cfgs.RekeyMethod)) + debug_write("__security_cfg, before, IEEE8021X="..tostring(cfgs.IEEE8021X)) + debug_write("__security_cfg, before, DefaultKeyID="..tostring(cfgs.DefaultKeyID)) + debug_write("__security_cfg, before, PMFMFPC="..tostring(cfgs.PMFMFPC)) + debug_write("__security_cfg, before, PMFMFPR="..tostring(cfgs.PMFMFPR)) + debug_write("__security_cfg, before, PMFSHA256="..tostring(cfgs.PMFSHA256)) + debug_write("__security_cfg, before, RADIUS_Server="..tostring(cfgs.RADIUS_Server)) + debug_write("__security_cfg, before, RADIUS_Port="..tostring(cfgs.RADIUS_Port)) + debug_write("__security_cfg, before, session_timeout_interval="..tostring(cfgs.session_timeout_interval)) + debug_write("__security_cfg, before, PMKCachePeriod="..tostring(cfgs.PMKCachePeriod)) + debug_write("__security_cfg, before, PreAuth="..tostring(cfgs.PreAuth)) + debug_write("__security_cfg, before, Wapiifname="..tostring(cfgs.Wapiifname)) + + -- Reset/Clear all necessary settings here. Later, these settings will be set as per AuthMode. + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "DISABLE") + cfgs.IEEE8021X = mtkwifi.token_set(cfgs.IEEE8021X, vif_idx, "0") + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "0") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "0") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + -- Update the settings which are not dependent on AuthMode + cfgs.HideSSID = mtkwifi.token_set(cfgs.HideSSID, vif_idx, http.formvalue("__hidessid") or "0") + cfgs.NoForwarding = mtkwifi.token_set(cfgs.NoForwarding, vif_idx, http.formvalue("__noforwarding") or "0") + cfgs.WmmCapable = mtkwifi.token_set(cfgs.WmmCapable, vif_idx, http.formvalue("__wmmcapable") or "0") + cfgs.TxRate = mtkwifi.token_set(cfgs.TxRate, vif_idx, http.formvalue("__txrate") or "0"); + cfgs.RekeyInterval = mtkwifi.token_set(cfgs.RekeyInterval, vif_idx, http.formvalue("__rekeyinterval") or "0"); + + local __authmode = http.formvalue("__authmode") or "Disable" + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, vif_idx, __authmode) + + if __authmode == "Disable" then + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, vif_idx, "OPEN") + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "NONE") + + elseif __authmode == "OPEN" or __authmode == "SHARED" or __authmode == "WEPAUTO" then + cfgs.WscModeOption = "0" + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "WEP") + cfgs = conf_wep_keys(cfgs,vif_idx) + + elseif __authmode == "Enhanced Open" then + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, vif_idx, "OWE") + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "AES") + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "1") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "1") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + elseif __authmode == "WPAPSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__encrypttype") or "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + + elseif __authmode == "WPAPSKWPA2PSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__encrypttype") or "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + cfgs.WpaMixPairCipher = "WPA_TKIP_WPA2_AES" + + elseif __authmode == "WPA2PSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__encrypttype") or "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, http.formvalue("__pmfmfpc") or "0") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, http.formvalue("__pmfmfpr") or "0") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, http.formvalue("__pmfsha256") or "0") + + elseif __authmode == "WPA3PSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "1") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "1") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + elseif __authmode == "WPA2PSKWPA3PSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "1") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "0") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + elseif __authmode == "WPA2" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__encrypttype") or "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + cfgs.RADIUS_Server = mtkwifi.token_set(cfgs.RADIUS_Server, vif_idx, http.formvalue("__radius_server") or "0") + cfgs.RADIUS_Port = mtkwifi.token_set(cfgs.RADIUS_Port, vif_idx, http.formvalue("__radius_port") or "0") + cfgs.session_timeout_interval = mtkwifi.token_set(cfgs.session_timeout_interval, vif_idx, http.formvalue("__session_timeout_interval") or "0") + cfgs.PMKCachePeriod = mtkwifi.token_set(cfgs.PMKCachePeriod, vif_idx, http.formvalue("__pmkcacheperiod") or "0") + cfgs.PreAuth = mtkwifi.token_set(cfgs.PreAuth, vif_idx, http.formvalue("__preauth") or "0") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, http.formvalue("__pmfmfpc") or "0") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, http.formvalue("__pmfmfpr") or "0") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, http.formvalue("__pmfsha256") or "0") + + elseif __authmode == "WPA3" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + cfgs.RADIUS_Server = mtkwifi.token_set(cfgs.RADIUS_Server, vif_idx, http.formvalue("__radius_server") or "0") + cfgs.RADIUS_Port = mtkwifi.token_set(cfgs.RADIUS_Port, vif_idx, http.formvalue("__radius_port") or "0") + cfgs.session_timeout_interval = mtkwifi.token_set(cfgs.session_timeout_interval, vif_idx, http.formvalue("__session_timeout_interval") or "0") + cfgs.PMKCachePeriod = mtkwifi.token_set(cfgs.PMKCachePeriod, vif_idx, http.formvalue("__pmkcacheperiod") or "0") + cfgs.PreAuth = mtkwifi.token_set(cfgs.PreAuth, vif_idx, http.formvalue("__preauth") or "0") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "1") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "1") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + elseif __authmode == "WPA3-192-bit" then + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, vif_idx, "WPA3-192") + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "GCMP256") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + cfgs.RADIUS_Server = mtkwifi.token_set(cfgs.RADIUS_Server, vif_idx, http.formvalue("__radius_server") or "0") + cfgs.RADIUS_Port = mtkwifi.token_set(cfgs.RADIUS_Port, vif_idx, http.formvalue("__radius_port") or "0") + cfgs.session_timeout_interval = mtkwifi.token_set(cfgs.session_timeout_interval, vif_idx, http.formvalue("__session_timeout_interval") or "0") + cfgs.PMKCachePeriod = mtkwifi.token_set(cfgs.PMKCachePeriod, vif_idx, http.formvalue("__pmkcacheperiod") or "0") + cfgs.PreAuth = mtkwifi.token_set(cfgs.PreAuth, vif_idx, http.formvalue("__preauth") or "0") + -- for DOT11W_PMF_SUPPORT + cfgs.PMFMFPC = mtkwifi.token_set(cfgs.PMFMFPC, vif_idx, "1") + cfgs.PMFMFPR = mtkwifi.token_set(cfgs.PMFMFPR, vif_idx, "1") + cfgs.PMFSHA256 = mtkwifi.token_set(cfgs.PMFSHA256, vif_idx, "0") + + elseif __authmode == "WPA1WPA2" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__encrypttype") or "AES") + cfgs.RekeyMethod = mtkwifi.token_set(cfgs.RekeyMethod, vif_idx, "TIME") + cfgs.RADIUS_Server = mtkwifi.token_set(cfgs.RADIUS_Server, vif_idx, http.formvalue("__radius_server") or "0") + cfgs.RADIUS_Port = mtkwifi.token_set(cfgs.RADIUS_Port, vif_idx, http.formvalue("__radius_port") or "1812") + cfgs.session_timeout_interval = mtkwifi.token_set(cfgs.session_timeout_interval, vif_idx, http.formvalue("__session_timeout_interval") or "0") + cfgs.PMKCachePeriod = mtkwifi.token_set(cfgs.PMKCachePeriod, vif_idx, http.formvalue("__pmkcacheperiod") or "0") + cfgs.PreAuth = mtkwifi.token_set(cfgs.PreAuth, vif_idx, http.formvalue("__preauth") or "0") + + elseif __authmode == "IEEE8021X" then + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, vif_idx, "OPEN") + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, http.formvalue("__8021x_wep") and "WEP" or "NONE") + cfgs.IEEE8021X = mtkwifi.token_set(cfgs.IEEE8021X, vif_idx, "1") + cfgs.RADIUS_Server = mtkwifi.token_set(cfgs.RADIUS_Server, vif_idx, http.formvalue("__radius_server") or "0") + cfgs.RADIUS_Port = mtkwifi.token_set(cfgs.RADIUS_Port, vif_idx, http.formvalue("__radius_port") or "0") + cfgs.session_timeout_interval = mtkwifi.token_set(cfgs.session_timeout_interval, vif_idx, http.formvalue("__session_timeout_interval") or "0") + + elseif __authmode == "WAICERT" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "SMS4") + cfgs.Wapiifname = mtkwifi.token_set(cfgs.Wapiifname, vif_idx, "br-lan") + -- cfgs.wapicert_asipaddr + -- cfgs.WapiAsPort + -- cfgs.wapicert_ascert + -- cfgs.wapicert_usercert + + elseif __authmode == "WAIPSK" then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, vif_idx, "SMS4") + -- cfgs.wapipsk_keytype + -- cfgs.wapipsk_prekey + end + + debug_write("__security_cfg, after, HideSSID="..tostring(cfgs.HideSSID)) + debug_write("__security_cfg, after, NoForwarding="..tostring(cfgs.NoForwarding)) + debug_write("__security_cfg, after, WmmCapable="..tostring(cfgs.WmmCapable)) + debug_write("__security_cfg, after, TxRate="..tostring(cfgs.TxRate)) + debug_write("__security_cfg, after, RekeyInterval="..tostring(cfgs.RekeyInterval)) + debug_write("__security_cfg, after, AuthMode="..tostring(cfgs.AuthMode)) + debug_write("__security_cfg, after, EncrypType="..tostring(cfgs.EncrypType)) + debug_write("__security_cfg, after, WscModeOption="..tostring(cfgs.WscModeOption)) + debug_write("__security_cfg, after, RekeyMethod="..tostring(cfgs.RekeyMethod)) + debug_write("__security_cfg, after, IEEE8021X="..tostring(cfgs.IEEE8021X)) + debug_write("__security_cfg, after, DefaultKeyID="..tostring(cfgs.DefaultKeyID)) + debug_write("__security_cfg, after, PMFMFPC="..tostring(cfgs.PMFMFPC)) + debug_write("__security_cfg, after, PMFMFPR="..tostring(cfgs.PMFMFPR)) + debug_write("__security_cfg, after, PMFSHA256="..tostring(cfgs.PMFSHA256)) + debug_write("__security_cfg, after, RADIUS_Server="..tostring(cfgs.RADIUS_Server)) + debug_write("__security_cfg, after, RADIUS_Port="..tostring(cfgs.RADIUS_Port)) + debug_write("__security_cfg, after, session_timeout_interval="..tostring(cfgs.session_timeout_interval)) + debug_write("__security_cfg, after, PMKCachePeriod="..tostring(cfgs.PMKCachePeriod)) + debug_write("__security_cfg, after, PreAuth="..tostring(cfgs.PreAuth)) + debug_write("__security_cfg, after, Wapiifname="..tostring(cfgs.Wapiifname)) +end + +function initialize_multiBssParameters(cfgs,vif_idx) + cfgs["WPAPSK"..vif_idx]="fixture1" + cfgs["Key1Type"]=mtkwifi.token_set(cfgs["Key1Type"],vif_idx,"0") + cfgs["Key2Type"]=mtkwifi.token_set(cfgs["Key2Type"],vif_idx,"0") + cfgs["Key3Type"]=mtkwifi.token_set(cfgs["Key3Type"],vif_idx,"0") + cfgs["Key4Type"]=mtkwifi.token_set(cfgs["Key4Type"],vif_idx,"0") + cfgs["RADIUS_Server"]=mtkwifi.token_set(cfgs["RADIUS_Server"],vif_idx,"0") + cfgs["RADIUS_Port"]=mtkwifi.token_set(cfgs["RADIUS_Port"],vif_idx,"1812") + cfgs["RADIUS_Key"..vif_idx]="ralink" + cfgs["DefaultKeyID"]=mtkwifi.token_set(cfgs["DefaultKeyID"],vif_idx,"1") + cfgs["IEEE8021X"]=mtkwifi.token_set(cfgs["IEEE8021X"],vif_idx,"0") + cfgs["WscConfMode"]=mtkwifi.token_set(cfgs["WscConfMode"],vif_idx,"0") + cfgs["PreAuth"]=mtkwifi.token_set(cfgs["PreAuth"],vif_idx,"0") + return cfgs +end + +function __wps_ap_pbc_start_all(ifname) + os.execute("iwpriv "..ifname.." set WscMode=2"); + os.execute("iwpriv "..ifname.." set WscGetConf=1"); +end + +function __wps_ap_pin_start_all(ifname, pincode) + os.execute("iwpriv "..ifname.." set WscMode=1") + os.execute("iwpriv "..ifname.." set WscPinCode="..pincode) + os.execute("iwpriv "..ifname.." set WscGetConf=1") +end + +local __restart_miniupnpd = function (devName,ifName) + if pcall(require, "wifi_services") then + -- OpenWRT + assert(type(devName) == type("")) + assert(type(ifName) == type("")) + local wifi_service = require("wifi_services") + debug_write("Call miniupnpd_chk() of wifi_services module") + miniupnpd_chk(devName,ifName,wifi_service) + else + -- LSDK + debug_write("Execute miniupnpd.sh script!") + os.execute("miniupnpd.sh init") + end +end + +local __restart_hotspot_daemon = function () + os.execute("killall hs") + os.execute("rm -rf /tmp/hotspot*") + -- As this function is executed in child environment, there is no need to spawn it using fork-exec method. + os.execute("hs -d 1 -v 2 -f/etc_ro/hotspot_ap.conf") +end + +local __restart_8021x = function (devName,ifName) + if pcall(require, "wifi_services") then + -- OpenWRT + assert(type(devName) == type("")) + assert(type(ifName) == type("")) + local ifPrefix = string.match(ifName,"([a-z]+)") + assert(type(ifPrefix) == type("")) + local wifi_service = require("wifi_services") + debug_write("Call d8021xd_chk() of wifi_services module") + d8021xd_chk(devName,ifPrefix,ifPrefix.."0",true) + else + -- LSDK + debug_write("Call mtkwifi.restart_8021x()") + mtkwifi.restart_8021x(devName) + end +end + +--Landen: CP functions from wireless for Ajax, reloading page is not required when DBDC ssid changed. +local __restart_all_daemons = function (devName,ifName) + __restart_8021x(devName,ifName) + __restart_hotspot_daemon() + __restart_miniupnpd(devName,ifName) +end + +function __apply_wifi_wpsconf(devs, devname, cfgs, diff) + local saved = cfgs.WscConfMode and cfgs.WscConfMode:gsub(";-(%d);-","%1") or "" + local applied = diff.WscConfMode and diff["WscConfMode"][2]:gsub(";-(%d);-","%1") or "" + local num_ifs = tonumber(cfgs.BssidNum) or 0 + + for idx=1, num_ifs do + local ifname = devs[devname]["vifs"][idx]["vifname"] + if mtkwifi.__any_wsc_enabled(saved:sub(idx,idx)) == 1 then + cfgs.WscConfStatus = mtkwifi.token_set(cfgs.WscConfStatus, idx, "2") + else + cfgs.WscConfStatus = mtkwifi.token_set(cfgs.WscConfStatus, idx, "1") + end + if (diff.WscConfMode) and saved:sub(idx,idx) ~= applied:sub(idx,idx) then + cfgs = mtkwifi.__restart_if_wps(devname, ifname, cfgs) + end + end + + -- __mtkwifi_save_profile() is called outside the loop because it is a high time consuming function. + __mtkwifi_save_profile(cfgs, devs[devname]["profile"], false) + + if diff.WscConfMode then + for idx=1, num_ifs do + local ifname = devs[devname]["vifs"][idx]["vifname"] + if saved:sub(idx,idx) ~= applied:sub(idx,idx) then + __restart_miniupnpd(devname, ifname) + end + end + end +end + +function __set_wifi_wpsconf(cfgs, wsc_enable, vif_idx) + debug_write("__set_wifi_wpsconf : wsc_enable = ",wsc_enable) + if(wsc_enable == "1") then + cfgs["WscConfMode"] = mtkwifi.token_set(cfgs["WscConfMode"], vif_idx, "7") + else + cfgs["WscConfMode"] = mtkwifi.token_set(cfgs["WscConfMode"], vif_idx, "0") + end + if(((http.formvalue("__authmode")=="OPEN") and + (http.formvalue("__encrypttype") == "WEP")) or + (http.formvalue("__hidessid") == "1")) then + cfgs.WscConfMode = mtkwifi.token_set(cfgs.WscConfMode, vif_idx, "0") + end + debug_write("__set_wifi_wpsconf : WscConfMode = ",cfgs["WscConfMode"]) +end + +function __update_mbss_para(cfgs, vif_idx) + debug_write(vif_idx) + cfgs.HT_STBC = mtkwifi.token_set(cfgs.HT_STBC, vif_idx, http.formvalue("__ht_stbc") or "0") + cfgs.HT_LDPC = mtkwifi.token_set(cfgs.HT_LDPC, vif_idx, http.formvalue("__ht_ldpc") or "0") + cfgs.VHT_STBC = mtkwifi.token_set(cfgs.VHT_STBC, vif_idx, http.formvalue("__vht_stbc") or "0") + cfgs.VHT_LDPC = mtkwifi.token_set(cfgs.VHT_LDPC, vif_idx, http.formvalue("__vht_ldpc") or "0") + cfgs.DLSCapable = mtkwifi.token_set(cfgs.DLSCapable, vif_idx, http.formvalue("__dls_capable") or "0") + cfgs.APSDCapable = mtkwifi.token_set(cfgs.APSDCapable, vif_idx, http.formvalue("__apsd_capable") or "0") + cfgs.FragThreshold = mtkwifi.token_set(cfgs.FragThreshold, vif_idx, http.formvalue("__frag_threshold") or "0") + cfgs.RTSThreshold = mtkwifi.token_set(cfgs.RTSThreshold, vif_idx, http.formvalue("__rts_threshold") or "0") + cfgs.VHT_SGI = mtkwifi.token_set(cfgs.VHT_SGI, vif_idx, http.formvalue("__vht_sgi") or "0") + cfgs.VHT_BW_SIGNAL = mtkwifi.token_set(cfgs.VHT_BW_SIGNAL, vif_idx, http.formvalue("__vht_bw_signal") or "0") + cfgs.HT_PROTECT = mtkwifi.token_set(cfgs.HT_PROTECT, vif_idx, http.formvalue("__ht_protect") or "0") + cfgs.HT_GI = mtkwifi.token_set(cfgs.HT_GI, vif_idx, http.formvalue("__ht_gi") or "0") + cfgs.HT_OpMode = mtkwifi.token_set(cfgs.HT_OpMode, vif_idx, http.formvalue("__ht_opmode") or "0") + cfgs.HT_AMSDU = mtkwifi.token_set(cfgs.HT_AMSDU, vif_idx, http.formvalue("__ht_amsdu") or "0") + cfgs.HT_AutoBA = mtkwifi.token_set(cfgs.HT_AutoBA, vif_idx, http.formvalue("__ht_autoba") or "0") + cfgs.IgmpSnEnable = mtkwifi.token_set(cfgs.IgmpSnEnable, vif_idx, http.formvalue("__igmp_snenable") or "0") + cfgs.WirelessMode = mtkwifi.token_set(cfgs.WirelessMode, vif_idx, http.formvalue("__wirelessmode") or "0") + cfgs.WdsEnable = mtkwifi.token_set(cfgs.WdsEnable, vif_idx, http.formvalue("__wdsenable") or "0") + cfgs.MuOfdmaDlEnable = mtkwifi.token_set(cfgs.MuOfdmaDlEnable, vif_idx, http.formvalue("__muofdma_dlenable") or "0") + cfgs.MuOfdmaUlEnable = mtkwifi.token_set(cfgs.MuOfdmaUlEnable, vif_idx, http.formvalue("__muofdma_ulenable") or "0") + cfgs.MuMimoDlEnable = mtkwifi.token_set(cfgs.MuMimoDlEnable, vif_idx, http.formvalue("__mumimo_dlenable") or "0") + cfgs.MuMimoUlEnable = mtkwifi.token_set(cfgs.MuMimoUlEnable, vif_idx, http.formvalue("__mumimo_ulenable") or "0") + +end + +function vif_cfg(dev, vif) + local devname, vifname = dev, vif + if not devname then devname = vif end + debug_write("devname="..devname) + debug_write("vifname="..(vifname or "")) + local devs = mtkwifi.get_all_devs() + local profile = devs[devname].profile + assert(profile) + + --local ssid_index; + --ssid_index = devs[devname]["vifs"][vifname].vifidx + + local cfgs = mtkwifi.load_profile(profile) + + for k,v in pairs(http.formvalue()) do + if type(v) == type("") or type(v) == type(0) then + nixio.syslog("debug", "post."..k.."="..tostring(v)) + else + nixio.syslog("debug", "post."..k.." invalid, type="..type(v)) + end + end + + -- sometimes vif_idx start from 0, like AccessPolicy0 + -- sometimes it starts from 1, like WPAPSK1. nice! + local vif_idx + local to_url + if http.formvalue("__action") == "vif_cfg_view" then + vif_idx = devs[devname]["vifs"][vifname].vifidx + debug_write("vif_idx=", vif_idx, devname, vifname) + to_url = luci.dispatcher.build_url("admin", "mtk", "wifi", "vif_cfg_view", devname, vifname) + elseif http.formvalue("__action") == "vif_add_view" then + cfgs.BssidNum = tonumber(cfgs.BssidNum) + 1 + vif_idx = tonumber(cfgs.BssidNum) + to_url = luci.dispatcher.build_url("admin", "mtk", "wifi") + -- initializing ; separated parameters for the new interface + cfgs = initialize_multiBssParameters(cfgs, vif_idx) + end + assert(vif_idx) + assert(to_url) + -- "__" should not be the prefix of a name if user wants to copy form value data directly to the dat file variable + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + nixio.syslog("err", "vif_cfg, invalid value type for "..k..","..type(v)) + elseif string.byte(k) ~= string.byte("_") then + debug_write("vif_cfg: Copying",k,v) + cfgs[k] = v or "" + end + end + + -- WDS + -- Update WdsXKey if respective WdsEncrypType is NONE + for i=0,3 do + if (cfgs["Wds"..i.."Key"] and cfgs["Wds"..i.."Key"] ~= "") and + ((not mtkwifi.token_get(cfgs["WdsEncrypType"],i+1,nil)) or + ("NONE" == mtkwifi.token_get(cfgs["WdsEncrypType"],i+1,nil))) then + cfgs["Wds"..i.."Key"] = "" + end + end + + cfgs["AccessPolicy"..vif_idx-1] = http.formvalue("__accesspolicy") + local t = mtkwifi.parse_mac(http.formvalue("__maclist")) + cfgs["AccessControlList"..vif_idx-1] = table.concat(t, ";") + + __security_cfg(cfgs, vif_idx) + __update_mbss_para(cfgs, vif_idx) + __set_wifi_wpsconf(cfgs, http.formvalue("WPSRadio"), vif_idx) + + __mtkwifi_save_profile(cfgs, profile, false) + if http.formvalue("__apply") then + mtkwifi.__run_in_child_env(__mtkwifi_reload, devname) + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",to_url)) + else + luci.http.redirect(to_url) + end +end + +function get_WPS_Info(devname, ifname) + local devs = mtkwifi.get_all_devs() + local ssid_index = devs[devname]["vifs"][ifname].vifidx + local profile = devs[devname].profile + assert(profile) + + local cfgs = mtkwifi.load_profile(profile) + + -- Create the applied settings backup file if it does not exist. + if not mtkwifi.exists(mtkwifi.__profile_applied_settings_path(profile)) then + os.execute("cp -f "..profile.." "..mtkwifi.__profile_applied_settings_path(profile)) + end + local applied_cfgs = mtkwifi.load_profile(mtkwifi.__profile_applied_settings_path(profile)) + + local WPS_details = {} + WPS_details = c_getCurrentWscProfile(ifname) + + if type(WPS_details) ~= "table" then + WPS_details["DRIVER_RSP"] = "NO" + else + WPS_details["DRIVER_RSP"] = "YES" + local isCfgsChanged = false -- To indicate that the settings have been changed by External Registrar. + local isBasicTabUpdateRequired = false + + if type(WPS_details["SSID"]) == "string" then + if applied_cfgs["SSID"..ssid_index] ~= WPS_details["SSID"] then + cfgs["SSID"..ssid_index] = WPS_details["SSID"] + isCfgsChanged = true + isBasicTabUpdateRequired = true + end + else + WPS_details["SSID"] = cfgs["SSID"..ssid_index] + end + + if type(WPS_details["AuthMode"]) == "string" then + local auth_mode_ioctl = WPS_details["AuthMode"]:gsub("%W",""):upper() + local auth_mode_applied = mtkwifi.token_get(applied_cfgs.AuthMode, ssid_index, "") + if auth_mode_applied ~= auth_mode_ioctl then + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, ssid_index, auth_mode_ioctl) + isCfgsChanged = true + isBasicTabUpdateRequired = true + end + else + WPS_details["AuthMode"] = mtkwifi.token_get(cfgs.AuthMode, ssid_index, "") + end + + if type(WPS_details["EncType"]) == "string" then + local enc_type_ioctl = WPS_details["EncType"]:upper() + local enc_type_applied = mtkwifi.token_get(applied_cfgs.EncrypType, ssid_index, "") + if enc_type_applied ~= enc_type_ioctl then + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, ssid_index, enc_type_ioctl) + isCfgsChanged = true + isBasicTabUpdateRequired = true + end + else + WPS_details["EncType"] = mtkwifi.token_get(cfgs.EncrypType, ssid_index, "") + end + + if type(WPS_details["WscWPAKey"]) == "string" then + if applied_cfgs["WPAPSK"..ssid_index] ~= WPS_details["WscWPAKey"] then + cfgs["WPAPSK"..ssid_index] = WPS_details["WscWPAKey"] + isCfgsChanged = true + isBasicTabUpdateRequired = true + end + else + WPS_details["WscWPAKey"] = cfgs["WPAPSK"..ssid_index] + end + + if type(WPS_details["DefKey"]) == "number" then + local def_key_applied = tonumber(mtkwifi.token_get(applied_cfgs.DefaultKeyID, ssid_index, "")) + if def_key_applied ~= WPS_details["DefKey"] then + cfgs.DefaultKeyID = mtkwifi.token_set(cfgs.DefaultKeyID, ssid_index, WPS_details["DefKey"]) + isCfgsChanged = true + end + else + WPS_details["DefKey"] = tonumber(mtkwifi.token_get(cfgs.DefaultKeyID, ssid_index, 0)) or "" + end + + if type(WPS_details["Conf"]) == "number" then + local wsc_conf_status_applied = tonumber(mtkwifi.token_get(applied_cfgs.WscConfStatus, ssid_index, "")) + if wsc_conf_status_applied ~= WPS_details["Conf"] then + cfgs.WscConfStatus = mtkwifi.token_set(cfgs.WscConfStatus, ssid_index, WPS_details["Conf"]) + isCfgsChanged = true + end + else + WPS_details["Conf"] = mtkwifi.token_get(cfgs.WscConfStatus, ssid_index, "") + end + + WPS_details["IS_BASIC_TAB_UPDATE_REQUIRED"] = isBasicTabUpdateRequired + + if isCfgsChanged then + -- Driver updates the *.dat file for following scenarios, + -- 1. When WPS Conf Status is not configured i.e. WscConfStatus is not set as 2, + -- and connection with a station is established i.e. where station acts as an External Registrar. + -- 2. When below settings are changed through External Registrar irrespective of WPS Conf Status + -- Update mtkwifi.__profile_applied_settings_path(profile) file with the + -- new settings to avoid display of "reload to apply changes" message. + applied_cfgs["WPAPSK"] = cfgs["WPAPSK"] + applied_cfgs["SSID"] = cfgs["SSID"] + applied_cfgs["SSID"..ssid_index] = cfgs["SSID"..ssid_index] + applied_cfgs["AuthMode"] = cfgs["AuthMode"] + applied_cfgs["EncrypType"] = cfgs["EncrypType"] + applied_cfgs["WPAPSK"..ssid_index] = cfgs["WPAPSK"..ssid_index] + applied_cfgs["DefaultKeyID"] = cfgs["DefaultKeyID"] + applied_cfgs["WscConfStatus"] = cfgs["WscConfStatus"] + mtkwifi.save_profile(applied_cfgs, mtkwifi.__profile_applied_settings_path(profile)) + end + end + http.write_json(WPS_details) +end + +function get_wifi_pin(ifname) + local pin = "" + pin = c_getApPin(ifname) + http.write_json(pin) +end + +function set_wifi_gen_pin(ifname,devname) + local devs = mtkwifi.get_all_devs() + local ssid_index = devs[devname]["vifs"][ifname].vifidx + local profile = devs[devname].profile + assert(profile) + + local cfgs = mtkwifi.load_profile(profile) + + os.execute("iwpriv "..ifname.." set WscGenPinCode") + + pin = c_getApPin(ifname) + cfgs["WscVendorPinCode"]=pin["genpincode"] + + --existing c code... done nothing for this segment as it read flash data and write to related .dat file. + -- no concept of nvram zones here + --if (nvram == RT2860_NVRAM) + -- do_system("ralink_init make_wireless_config rt2860"); + --else + -- do_system("ralink_init make_wireless_config rtdev"); + __mtkwifi_save_profile(cfgs, profile, true) + http.write_json(pin) +end + +function set_wifi_wps_oob(devname, ifname) + local SSID, mac = "" + local ssid_index = 0 + local devs = mtkwifi.get_all_devs() + local profile = devs[devname].profile + assert(profile) + + local cfgs = mtkwifi.load_profile(profile) + + ssid_index = devs[devname]["vifs"][ifname].vifidx + mac = c_get_macaddr(ifname) + + if (mac["macaddr"] ~= "") then + SSID = "RalinkInitAP"..(ssid_index-1).."_"..mac["macaddr"] + else + SSID = "RalinkInitAP"..(ssid_index-1).."_unknown" + end + + cfgs["SSID"..ssid_index]=SSID + cfgs.WscConfStatus = mtkwifi.token_set(cfgs.WscConfStatus, ssid_index, "1") + cfgs.AuthMode = mtkwifi.token_set(cfgs.AuthMode, ssid_index, "WPA2PSK") + cfgs.EncrypType = mtkwifi.token_set(cfgs.EncrypType, ssid_index, "AES") + cfgs.DefaultKeyID = mtkwifi.token_set(cfgs.DefaultKeyID, ssid_index, "2") + + cfgs["WPAPSK"..ssid_index]="fixture1" + cfgs["WPAPSK"]="" + cfgs.IEEE8021X = mtkwifi.token_set(cfgs.IEEE8021X, ssid_index, "0") + + os.execute("iwpriv "..ifname.." set SSID="..SSID ) + debug_write("iwpriv "..ifname.." set SSID="..SSID ) + os.execute("iwpriv "..ifname.." set AuthMode=WPA2PSK") + debug_write("iwpriv "..ifname.." set AuthMode=WPA2PSK") + os.execute("iwpriv "..ifname.." set EncrypType=AES") + debug_write("iwpriv "..ifname.." set EncrypType=AES") + os.execute("iwpriv "..ifname.." set WPAPSK=fixture1") + debug_write("iwpriv "..ifname.." set WPAPSK=fixture1") + os.execute("iwpriv "..ifname.." set SSID="..SSID) + debug_write("iwpriv "..ifname.." set SSID="..SSID) + + cfgs = mtkwifi.__restart_if_wps(devname, ifname, cfgs) + __mtkwifi_save_profile(cfgs, profile, true) + + mtkwifi.__run_in_child_env(__restart_all_daemons, devname, ifname) + + os.execute("iwpriv "..ifname.." set WscConfStatus=1") + debug_write("iwpriv "..ifname.." set WscConfStatus=1") + + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "mtk", "wifi", "vif_cfg_view", devname, ifname) + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",url_to_visit_after_reload)) +end + +function set_wifi_do_wps(ifname, devname, wsc_pin_code_w) + local devs = mtkwifi.get_all_devs() + local ssid_index = devs[devname]["vifs"][ifname].vifidx + local profile = devs[devname].profile + local wsc_mode = 0 + local wsc_conf_mode + assert(profile) + + local cfgs = mtkwifi.load_profile(profile) + + if(wsc_pin_code_w == "nopin") then + wsc_mode=2 + else + wsc_mode=1 + end + + wsc_conf_mode = mtkwifi.token_get(cfgs["WscConfMode"], ssid_index, nil) + + if (wsc_conf_mode == 0) then + print("{\"wps_start\":\"WPS_NOT_ENABLED\"}") + DBG_MSG("WPS is not enabled before do PBC/PIN.\n") + return + end + + if (wsc_mode == 1) then + __wps_ap_pin_start_all(ifname, wsc_pin_code_w) + + elseif (wsc_mode == 2) then + __wps_ap_pbc_start_all(ifname) + else + http.write_json("{\"wps_start\":\"NG\"}") + return + end + cfgs["WscStartIF"] = ifname + + -- execute wps_action.lua file to send signal for current interface + os.execute("lua wps_action.lua "..ifname) + + http.write_json("{\"wps_start\":\"OK\"}") +end + +function get_wps_security(ifname, devname) + local devs = mtkwifi.get_all_devs() + local ssid_index = devs[devname]["vifs"][ifname].vifidx + local profile = devs[devname].profile + assert(profile) + local output = {} + local cfgs = mtkwifi.load_profile(profile) + + output["AuthMode"] = mtkwifi.token_get(cfgs.AuthMode,ssid_index) + output["IEEE8021X"] = mtkwifi.token_get(cfgs.IEEE8021X,ssid_index) + + http.write_json(output) +end + +function apcli_get_wps_status(ifname, devname) + local output = {} + local ssid_index = 0 + local devs = mtkwifi.get_all_devs() + local profile = devs[devname].profile + assert(profile) + + -- apcli interface has a different structure as compared to other vifs + ssid_index = devs[devname][ifname].vifidx + output = c_apcli_get_wps_status(ifname) + if (output.wps_port_secured == "YES") then + local cfgs = mtkwifi.load_profile(profile) + cfgs.ApCliSsid = mtkwifi.token_set(cfgs.ApCliSsid, ssid_index, output.enr_SSID) + cfgs.ApCliEnable = mtkwifi.token_set(cfgs.ApCliEnable, ssid_index, "1") + cfgs.ApCliAuthMode = mtkwifi.token_set(cfgs.ApCliAuthMode, ssid_index, output.enr_AuthMode) + cfgs.ApCliEncrypType = mtkwifi.token_set(cfgs.ApCliEncrypType, ssid_index, output.enr_EncrypType) + cfgs.ApCliDefaultKeyID = mtkwifi.token_set(cfgs.ApCliDefaultKeyID, ssid_index, output.enr_DefaultKeyID) + cfgs.Channel = mtkwifi.read_pipe("iwconfig "..ifname.." | grep Channel | cut -d = -f 2 | cut -d \" \" -f 1") + debug_write("iwconfig "..ifname.." | grep Channel | cut -d = -f 2 | cut -d \" \" -f 1") + + if(output.enr_EncrypType == "WEP") then + for i = 1, 4 do + cfgs["ApCliKey"..i.."Type"] = mtkwifi.token_set(cfgs["ApCliKey"..i.."Type"], ssid_index, output["Key"..i.."Type"]) + end + if(ssid_index == "0") then + cfgs["ApCliKey"..output.enr_DefaultKeyID.."Str"] = output.enr_KeyStr + else + cfgs["ApCliKey"..output.enr_DefaultKeyID.."Str"..ssid_index] = output.enr_KeyStr + end + elseif(output.enr_EncrypType == "TKIP") or (output.enr_EncrypType == "AES") or (output.enr_EncrypType == "TKIPAES") then + if(output.enr_AuthMode ~= "WPAPSKWPA2PSK") then + cfgs["ApCliWPAPSK"] = output.enr_WPAPSK + end + end + __mtkwifi_save_profile(cfgs, profile, true) + end + http.write_json(output); +end + +function string.tohex(str) + return (str:gsub('.', function (c) + return string.format('%02X', string.byte(c)) + end)) +end + +function unencode_ssid(raw_ssid) + local c + local output = "" + local convertNext = 0 + for c in raw_ssid:gmatch"." do + if(convertNext == 0) then + if(c == '+') then + output = output..' ' + elseif(c == '%') then + convertNext = 1 + else + output = output..c + end + else + output = output..string.tohex(c) + convertNext = 0 + end + end + return output +end + +function decode_ssid(raw_ssid) + local output = raw_ssid + output = output:gsub("&", "&") + output = output:gsub("<", "<") + output = output:gsub(">", ">") + output = output:gsub(""", "\"") + output = output:gsub("'", "'") + output = output:gsub(" ", " ") + for codenum in raw_ssid:gmatch("&#(%d+);") do + output = output:gsub("&#"..codenum..";", string.char(tonumber(codenum))) + end + return output +end + +function apcli_do_enr_pin_wps(ifname, devname, raw_ssid) + local target_ap_ssid = "" + local ret_value = {} + if(raw_ssid == "") then + ret_value["apcli_do_enr_pin_wps"] = "GET_SSID_NG" + end + ret_value["raw_ssid"] = raw_ssid + target_ap_ssid = decode_ssid(raw_ssid) + target_ap_ssid = ''..mtkwifi.__handleSpecialChars(target_ap_ssid) + ret_value["target_ap_ssid"] = target_ap_ssid + if(target_ap_ssid == "") then + ret_value["apcli_do_enr_pin_wps"] = "GET_SSID_NG" + else + ret_value["apcli_do_enr_pin_wps"] = "OK" + end + os.execute("ifconfig "..ifname.." up") + debug_write("ifconfig "..ifname.." up") + os.execute("brctl addif br0 "..ifname) + debug_write("brctl addif br0 "..ifname) + os.execute("brctl addif br-lan "..ifname) + debug_write("brctl addif br-lan "..ifname) + os.execute("iwpriv "..ifname.." set ApCliAutoConnect=1") + os.execute("iwpriv "..ifname.." set ApCliEnable=1") + debug_write("iwpriv "..ifname.." set ApCliEnable=1") + --os.execute("iwpriv "..ifname.." set WscConfMode=0") + os.execute("iwpriv "..ifname.." set WscConfMode=1") + debug_write("iwpriv "..ifname.." set WscConfMode=1") + os.execute("iwpriv "..ifname.." set WscMode=1") + debug_write("iwpriv "..ifname.." set WscMode=1") + os.execute("iwpriv "..ifname.." set ApCliWscSsid=\""..target_ap_ssid.."\"") + debug_write("iwpriv "..ifname.." set ApCliWscSsid=\""..target_ap_ssid.."\"") + os.execute("iwpriv "..ifname.." set WscGetConf=1") + debug_write("iwpriv "..ifname.." set WscGetConf=1") + -- check interface value to correlate with nvram as values will be like apclixxx + os.execute("wps_action.lua "..ifname) + http.write_json(ret_value) +end + +function apcli_do_enr_pbc_wps(ifname, devname) + local ret_value = {} + + --os.execute("iwpriv "..ifname.." set ApCliAutoConnect=1") + --os.execute("iwpriv "..ifname.." set ApCliEnable=1") + --os.execute("ifconfig "..ifname.." up") + --os.execute("brctl addif br0 "..ifname) + --os.execute("iwpriv "..ifname.." set WscConfMode=0") + os.execute("iwpriv "..ifname.." set WscConfMode=1") + os.execute("iwpriv "..ifname.." set WscMode=2") + os.execute("iwpriv "..ifname.." set WscGetConf=1") + -- check interface value to correlate with nvram as values will be like apclixxx + os.execute("wps_action.lua "..ifname) + + --debug_write("iwpriv "..ifname.." set ApCliEnable=1") + --debug_write("brctl addif br0 "..ifname) + --debug_write("ifconfig "..ifname.." up") + debug_write("iwpriv "..ifname.." set WscConfMode=1") + debug_write("iwpriv "..ifname.." set WscMode=2") + debug_write("iwpriv "..ifname.." set WscGetConf=1") + ret_value["apcli_do_enr_pbc_wps"] = "OK" + http.write_json(ret_value) +end + +function apcli_cancel_wps(ifname) + local ret_value = {} + os.execute("iwpriv "..ifname.." set WscStop=1") + os.execute("miniupnpd.sh init") + -- check interface value to correlate with nvram as values will be like apclixxx + os.execute("wps_action.lua "..ifname) + ret_value["apcli_cancel_wps"] = "OK" + http.write_json(ret_value) +end + +function apcli_wps_gen_pincode(ifname) + local ret_value = {} + os.execute("iwpriv "..ifname.." set WscGenPinCode") + ret_value["apcli_wps_gen_pincode"] = "OK" + http.write_json(ret_value) +end + +function apcli_wps_get_pincode(ifname) + local output = c_apcli_wps_get_pincode(ifname) + http.write_json(output) +end + +function get_apcli_conn_info(ifname) + local rsp = {} + if not ifname then + rsp["conn_state"]="Disconnected" + else + local flags = tonumber(mtkwifi.read_pipe("cat /sys/class/net/"..ifname.."/flags 2>/dev/null")) or 0 + rsp["infc_state"] = flags%2 == 1 and "up" or "down" + local iwapcli = mtkwifi.read_pipe("iwconfig "..ifname.." | grep ESSID 2>/dev/null") + local ssid = string.match(iwapcli, "ESSID:\"(.*)\"") + iwapcli = mtkwifi.read_pipe("iwconfig "..ifname.." | grep 'Access Point' 2>/dev/null") + local bssid = string.match(iwapcli, "%x%x:%x%x:%x%x:%x%x:%x%x:%x%x") + if not ssid or ssid == "" then + rsp["conn_state"]= "Disconnected" + else + rsp["conn_state"] = "Connected" + rsp["ssid"] = ssid + rsp["bssid"] = bssid or "N/A" + end + end + http.write_json(rsp) +end + +function sta_info(ifname) + local output = {} + local stalist = c_StaInfo(ifname) + + local count = 0 + for _ in pairs(stalist) do count = count + 1 end + + for i=0, count - 1 do + table.insert(output, stalist[i]) + end + http.write_json(output) +end + +function apcli_scan(ifname) + local aplist = mtkwifi.scan_ap(ifname) + local convert=""; + for i=1, #aplist do + convert = c_convert_string_display(aplist[i]["ssid"]) + aplist[i]["original_ssid"] = aplist[i]["ssid"] + aplist[i]["ssid"] = convert["output"] + end + http.write_json(aplist) +end + +function get_station_list() + http.write("get_station_list") +end + +function reset_wifi(devname) + if devname then + os.execute("cp -f /rom/etc/wireless/"..devname.."/ /etc/wireless/") + else + os.execute("cp -rf /rom/etc/wireless /etc/") + end + return luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi")) +end + +function reload_wifi(devname) + profiles = mtkwifi.search_dev_and_profile() + path = profiles[devname] + mtkwifi.__run_in_child_env(__mtkwifi_reload, devname) + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "mtk", "wifi") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",url_to_visit_after_reload)) +end + +function get_raw_profile() + local sid = http.formvalue("sid") + http.write_json("get_raw_profile") +end + +function get_country_region_list() + local mode = http.formvalue("mode") + local cr_list; + + if mtkwifi.band(mode) == "5G" then + cr_list = mtkwifi.CountryRegionList_5G_All + elseif mtkwifi.band(mode) == "6G" then + cr_list = mtkwifi.CountryRegionList_6G_All + else + cr_list = mtkwifi.CountryRegionList_2G_All + end + + http.write_json(cr_list) +end + +function remove_ch_by_region(ch_list, region) + for i = #ch_list,2,-1 do + if not ch_list[i].region[region] then + table.remove(ch_list, i) + end + end +end + +function get_channel_list() + local mode = http.formvalue("mode") + local region = tonumber(http.formvalue("country_region")) or 1 + local ch_list + + if mtkwifi.band(mode) == "5G" then + ch_list = mtkwifi.ChannelList_5G_All + elseif mtkwifi.band(mode) == "6G" then + ch_list = mtkwifi.ChannelList_6G_All + else + ch_list = mtkwifi.ChannelList_2G_All + end + + remove_ch_by_region(ch_list, region) + http.write_json(ch_list) +end + +function get_HT_ext_channel_list() + local mode = http.formvalue("mode") + local ch_cur = tonumber(http.formvalue("ch_cur")) + local region = tonumber(http.formvalue("country_region")) or 1 + local ext_ch_list = {} + + if mtkwifi.band(mode) == "6G" then -- 6G Channel + local ch_list = mtkwifi.ChannelList_6G_All + local ext_ch_idx = -1 + local len = 0 + + for k, v in ipairs(ch_list) do + len = len + 1 + if v.channel == ch_cur then + ext_ch_idx = (k % 2 == 0) and k + 1 or k - 1 + end + end + + if ext_ch_idx > 0 and ext_ch_idx < len and ch_list[ext_ch_idx].region[region] then + ext_ch_list[1] = {} + ext_ch_list[1].val = ext_ch_idx % 2 + ext_ch_list[1].text = ch_list[ext_ch_idx].text + end + + elseif mtkwifi.band(mode) == "2.4G" then -- 2.4G Channel + local ch_list = mtkwifi.ChannelList_2G_All + local below_ch = ch_cur - 4 + local above_ch = ch_cur + 4 + local i = 1 + + if below_ch > 0 and ch_list[below_ch + 1].region[region] then + ext_ch_list[i] = {} + ext_ch_list[i].val = 0 + ext_ch_list[i].text = ch_list[below_ch + 1].text + i = i + 1 + end + + if above_ch <= 14 and ch_list[above_ch + 1].region[region] then + ext_ch_list[i] = {} + ext_ch_list[i].val = 1 + ext_ch_list[i].text = ch_list[above_ch + 1].text + end + else -- 5G Channel + local ch_list = mtkwifi.ChannelList_5G_All + local ext_ch_idx = -1 + local len = 0 + + for k, v in ipairs(ch_list) do + len = len + 1 + if v.channel == ch_cur then + ext_ch_idx = (k % 2 == 0) and k + 1 or k - 1 + end + end + + if ext_ch_idx > 0 and ext_ch_idx < len and ch_list[ext_ch_idx].region[region] then + ext_ch_list[1] = {} + ext_ch_list[1].val = ext_ch_idx % 2 + ext_ch_list[1].text = ch_list[ext_ch_idx].text + end + end + + http.write_json(ext_ch_list) +end + +function get_5G_2nd_80Mhz_channel_list() + local ch_cur = tonumber(http.formvalue("ch_cur")) + local region = tonumber(http.formvalue("country_region")) + local ch_list = mtkwifi.ChannelList_5G_2nd_80MHZ_ALL + local ch_list_5g = mtkwifi.ChannelList_5G_All + local i, j, test_ch, test_idx + local bw80_1st_idx = -1 + + -- remove adjacent freqencies starting from list tail. + for i = #ch_list,1,-1 do + for j = 0,3 do + if ch_list[i].channel == -1 then + break + end + + test_ch = ch_list[i].channel + j * 4 + test_idx = ch_list[i].chidx + j + + if test_ch == ch_cur then + if i + 1 <= #ch_list and ch_list[i + 1] then + table.remove(ch_list, i + 1) + end + table.remove(ch_list, i) + bw80_1st_idx = i + break + end + + if i == (bw80_1st_idx - 1) or (not ch_list_5g[test_idx].region[region]) then + table.remove(ch_list, i) + break + end + end + end + + -- remove unused channel. + for i = #ch_list,1,-1 do + if ch_list[i].channel == -1 then + table.remove(ch_list, i) + end + end + http.write_json(ch_list) +end + +function webcmd() + local cmd = http.formvalue("cmd") + if cmd then + local result = mtkwifi.read_pipe(tostring(cmd).." 2>&1") + result = result:gsub("<", "<") + http.write(tostring(result)) + else + http.write_json(http.formvalue()) + end +end + +function net_cfg() + http.write_json(http.formvalue()) +end + +function apcli_cfg(dev, vif) + local devname = dev + debug_write(devname) + local profiles = mtkwifi.search_dev_and_profile() + debug_write(profiles[devname]) + assert(profiles[devname]) + + local cfgs = mtkwifi.load_profile(profiles[devname]) + + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + nixio.syslog("err", "apcli_cfg, invalid value type for "..k..","..type(v)) + elseif string.byte(k) ~= string.byte("_") then + cfgs[k] = v or "" + end + end + + if cfgs['ApCliEnable'] == '1' then + os.execute("brctl addif br-lan "..vif) + end + + -- http.write_json(http.formvalue()) + + -- Mediatek Adaptive Network + --[=[ moved to a separated page + if cfgs.ApCliEzEnable then + cfgs.EzEnable = cfgs.ApCliEzEnable + cfgs.ApMWDS = cfgs.ApCliMWDS + cfgs.EzConfStatus = cfgs.ApCliEzConfStatus + cfgs.EzOpenGroupID = cfgs.ApCliEzOpenGroupID + if http.formvalue("__group_id_mode") == "0" then + cfgs.EzGroupID = cfgs.ApCliEzGroupID + cfgs.EzGenGroupID = "" + cfgs.ApCliEzGenGroupID = "" + else + cfgs.EzGroupID = "" + cfgs.ApCliEzGroupID = "" + cfgs.EzGenGroupID = cfgs.ApCliEzGenGroupID + end + -- if dbdc + -- os.execute("app_ez &") + -- os.execute("ManDaemon ") + end + ]=] + __mtkwifi_save_profile(cfgs, profiles[devname], false) + + -- M.A.N Push parameters + -- They are not part of wifi profile, we save it into /etc/man.conf. + + --[=[ moved to a separated page + local man_ssid = http.formvalue("__man_ssid_"..vifname) + local man_pass = http.formvalue("__man_pass_"..vifname) + local man_auth = http.formvalue("__man_auth_"..vifname) or "" + + if man_ssid and man_pass then + local fp = io.open("/etc/man."..vifname..".conf", "w+") + fp:write("__man_ssid_"..vifname.."="..man_ssid.."\n") + fp:write("__man_pass_"..vifname.."="..man_pass.."\n") + fp:write("__man_auth_"..vifname.."="..man_auth.."\n") + fp:close() + end + ]=] + + -- commented, do not connect by default + --[=[ + os.execute("iwpriv apcli0 set ApCliEnable=0") + os.execute("iwpriv apcli0 set Channel="..cfgs.Channel) + os.execute("iwpriv apcli0 set ApCliAuthMode="..cfgs.ApCliAuthMode) + os.execute("iwpriv apcli0 set ApCliEncrypType="..cfgs.ApCliEncrypType) + if cfgs.ApCliAuthMode == "WEP" then + os.execute("#iwpriv apcli0 set ApCliDefaultKeyID="..cfgs.ApCliDefaultKeyID) + os.execute("#iwpriv apcli0 set ApCliKey1="..cfgs.ApCliKey1Str) + elseif cfgs.ApCliAuthMode == "WPAPSK" + or cfgs.ApCliAuthMode == "WPA2PSK" + or cfgs.ApCliAuthMode == "WPAPSKWPA2PSK" then + os.execute("iwpriv apcli0 set ApCliWPAPSK="..cfgs.ApCliWPAPSK) + end + -- os.execute("iwpriv apcli0 set ApCliWirelessMode=") + os.execute("iwpriv apcli0 set ApCliSsid="..cfgs.ApCliSsid) + os.execute("iwpriv apcli0 set ApCliEnable=1") + ]=] + if http.formvalue("__apply") then + mtkwifi.__run_in_child_env(__mtkwifi_reload, devname) + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "mtk", "wifi", "apcli_cfg_view", dev, vif) + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",url_to_visit_after_reload)) + else + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "apcli_cfg_view", dev, vif)) + end +end + +function apcli_connect(dev, vif) + -- dev_vif can be + -- 1. mt7620.apcli0 # simple case + -- 2. mt7615e.1.apclix0 # multi-card + -- 3. mt7615e.1.2G.apclix0 # multi-card & multi-profile + local devname,vifname = dev, vif + debug_write("devname=", dev, "vifname=", vif) + local profiles = mtkwifi.search_dev_and_profile() + debug_write(profiles[devname]) + assert(profiles[devname]) + local cfgs = mtkwifi.load_profile(profiles[devname]) + cfgs.ApCliEnable = "1" + __mtkwifi_save_profile(cfgs, profiles[devname], true) + os.execute("ifconfig "..vifname.." up") + os.execute("brctl addif br-lan "..vifname) + os.execute("iwpriv "..vifname.." set MACRepeaterEn="..cfgs.MACRepeaterEn) + os.execute("iwpriv "..vifname.." set ApCliEnable=0") + os.execute("iwpriv "..vifname.." set Channel="..cfgs.Channel) + os.execute("iwpriv "..vifname.." set ApCliAuthMode="..cfgs.ApCliAuthMode) + os.execute("iwpriv "..vifname.." set ApCliEncrypType="..cfgs.ApCliEncrypType) + if cfgs.ApCliEncrypType == "WEP" then + os.execute("iwpriv "..vifname.." set ApCliDefaultKeyID="..cfgs.ApCliDefaultKeyID) + if (cfgs.ApCliDefaultKeyID == "1") then + os.execute("iwpriv "..vifname.." set ApCliKey1=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliKey1Str).."\"") + elseif (cfgs.ApCliDefaultKeyID == "2") then + os.execute("iwpriv "..vifname.." set ApCliKey2=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliKey2Str).."\"") + elseif (cfgs.ApCliDefaultKeyID == "3") then + os.execute("iwpriv "..vifname.." set ApCliKey3=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliKey3Str).."\"") + elseif (cfgs.ApCliDefaultKeyID == "4") then + os.execute("iwpriv "..vifname.." set ApCliKey4=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliKey4Str).."\"") + end + elseif cfgs.ApCliAuthMode == "WPAPSK" + or cfgs.ApCliAuthMode == "WPA2PSK" + or cfgs.ApCliAuthMode == "WPAPSKWPA2PSK" then + os.execute("iwpriv "..vifname.." set ApCliWPAPSK=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliWPAPSK).."\"") + end + os.execute("iwpriv "..vifname.." set ApCliSsid=\""..mtkwifi.__handleSpecialChars(cfgs.ApCliSsid).."\"") + os.execute("iwpriv "..vifname.." set ApCliEnable=1") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi")) +end + +function apcli_disconnect(dev, vif) + -- dev_vif can be + -- 1. mt7620.apcli0 # simple case + -- 2. mt7615e.1.apclix0 # multi-card + -- 3. mt7615e.1.2G.apclix0 # multi-card & multi-profile + local devname,vifname = dev, vif + debug_write("devname=", dev, "vifname", vif) + debug_write(devname) + debug_write(vifname) + local profiles = mtkwifi.search_dev_and_profile() + debug_write(profiles[devname]) + assert(profiles[devname]) + local cfgs = mtkwifi.load_profile(profiles[devname]) + cfgs.ApCliEnable = "1" + __mtkwifi_save_profile(cfgs, profiles[devname], true) + os.execute("iwpriv "..vifname.." set ApCliEnable=0") + os.execute("ifconfig "..vifname.." down") + os.execute("brctl delif br-lan "..vifname) + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi")) +end + +-- Mediatek Adaptive Network +function man_cfg() + local mtkwifi = require("mtkwifi") + local profiles = mtkwifi.search_dev_and_profile() + + for k,v in pairs(http.formvalue()) do + debug_write(k.."="..v) + end + + + for dev,profile in pairs(profiles) do + debug_write(dev.."=2======="..profile) + local cfgs = mtkwifi.load_profile(profile) + + if cfgs.ApCliEzEnable then + + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + nixio.syslog("err", "man_cfg, invalid value type for "..k..","..type(v)) + elseif string.byte(k) ~= string.byte("_") then + cfgs[k] = v or "" + end + end + + debug_write(tostring(http.formvalue("__"..dev.."_ezsetup"))) + cfgs.ApCliEzEnable = http.formvalue("__"..dev.."_ezsetup") or "0" + + -- Yes this is bad. LSDK insists on this. + if cfgs.ApCliEzEnable == "1" then + cfgs.ApCliEnable = "1" + cfgs.ApCliMWDS = "1" + cfgs.ApCliAuthMode = "WPS2PSK" + cfgs.ApCliEncrypType = AES + cfgs.ApCliWPAPSK = "fixture1" + cfgs.AuthMode = "WPA2PSK" + cfgs.EncrypType = "AES" + cfgs.RekeyMethod = "TIME" + cfgs.WPAPSK1 = "" + cfgs.RegroupSupport = "1;1" + end + + if http.formvalue("__group_id_mode") == "0" then + cfgs.EzGroupID = cfgs.ApCliEzGroupID + cfgs.EzGenGroupID = "" + cfgs.ApCliEzGenGroupID = "" + else + cfgs.EzGroupID = "" + cfgs.ApCliEzGroupID = "" + cfgs.EzGenGroupID = cfgs.ApCliEzGenGroupID + end + + cfgs.EzEnable = cfgs.ApCliEzEnable + cfgs.ApMWDS = cfgs.ApCliMWDS + cfgs.EzConfStatus = cfgs.ApCliEzConfStatus + cfgs.EzOpenGroupID = cfgs.ApCliEzOpenGroupID + end + __mtkwifi_save_profile(cfgs, profile, false) + end + + if http.formvalue("__apply") then + mtkwifi.__run_in_child_env(__mtkwifi_reload) + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "mtk", "man") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",url_to_visit_after_reload)) + else + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "man")) + end +end + +function apply_power_boost_settings() + local devname = http.formvalue("__devname") + local ret_status = {} + local devs = mtkwifi.get_all_devs() + local dev = {} + for _,v in ipairs(devs) do + if v.devname == devname then + dev = v + break + end + end + if next(dev) == nil then + ret_status["status"]= "Device "..(devname or "").." not found!" + elseif not dev.isPowerBoostSupported then + ret_status["status"]= "Power Boost feature is not supported by "..(devname or "").." Device!" + else + local cfgs = mtkwifi.load_profile(dev.profile) + if type(cfgs) ~= "table" or next(cfgs) == nil then + ret_status["status"]= "Profile settings file not found!" + else + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + debug_write("ERROR: [apply_power_boost_settings] String expected; Got"..type(v).."for"..k.."key") + ret_status["status"]= "Power Boost settings are of incorrect type!" + break + elseif string.byte(k) ~= string.byte("_") then + cfgs[k] = v or "" + end + end + if next(ret_status) == nil then + if type(dev.vifs) ~= "table" or next(dev.vifs) == nil or not cfgs.BssidNum or cfgs.BssidNum == "0" then + ret_status["status"]= "No Wireless Interfaces has been added yet!" + elseif cfgs.PowerUpenable ~= "1" then + ret_status["status"]= "Power Boost feature is not enabled!" + else + local up_vif_name_list = {} + for idx,vif in ipairs(dev.vifs) do + if vif.state == "up" and vif.vifname ~= nil and vif.vifname ~= "" and type(vif.vifname) == "string" then + up_vif_name_list[idx] = vif.vifname + end + end + if next(up_vif_name_list) == nil then + ret_status["status"]= "No Wireless Interfaces is up!" + else + for _,vifname in ipairs(up_vif_name_list) do + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=0:"..cfgs.PowerUpCckOfdm) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=1:"..cfgs.PowerUpHT20) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=2:"..cfgs.PowerUpHT40) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=3:"..cfgs.PowerUpVHT20) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=4:"..cfgs.PowerUpVHT40) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=5:"..cfgs.PowerUpVHT80) + os.execute("iwpriv "..vifname.." set TxPowerBoostCtrl=6:"..cfgs.PowerUpVHT160) + os.execute("sleep 1") -- Wait for 1 second to let driver process the above data + end + __mtkwifi_save_profile(cfgs, dev.profile, true) + ret_status["status"]= "SUCCESS" + end + end + end + end + end + http.write_json(ret_status) +end + +function get_bssid_num(devName) + local ret_status = {} + local profiles = mtkwifi.search_dev_and_profile() + for dev,profile in pairs(profiles) do + if devName == dev then + local cfgs = mtkwifi.load_profile(profile) + if type(cfgs) ~= "table" or next(cfgs) == nil then + ret_status["status"]= "Profile settings file not found!" + else + ret_status["status"] = "SUCCESS" + ret_status["bssidNum"] = cfgs.BssidNum + end + break + end + end + if next(ret_status) == nil then + ret_status["status"]= "Device "..(devName or "").." not found!" + end + http.write_json(ret_status) +end + +local exec_reset_to_defaults_cmd = function (devname) + if devname then + os.execute("wifi reset "..devname) + else + os.execute("wifi reset") + end +end + +function reset_to_defaults(devname) + mtkwifi.__run_in_child_env(exec_reset_to_defaults_cmd, devname) + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",mtkwifi.get_referer_url())) +end + +local exec_reset_to_default_easymesh_cmd = function () + -- OpenWRT + if mtkwifi.exists("/usr/bin/EasyMesh_openwrt.sh") then + os.execute("/usr/bin/EasyMesh_openwrt.sh default") + elseif mtkwifi.exists("/usr/bin/EasyMesh_7622.sh") then + os.execute("/usr/bin/EasyMesh_7622.sh default") + elseif mtkwifi.exists("/usr/bin/EasyMesh_7629.sh") then + os.execute("/usr/bin/EasyMesh_7629.sh default") + end + -- LSDK + if mtkwifi.exists("/sbin/EasyMesh.sh") then + os.execute("EasyMesh.sh default") + end +end + +function reset_to_default_easymesh() + mtkwifi.__run_in_child_env(exec_reset_to_default_easymesh_cmd) + + if mtkwifi.exists("/etc/dpp_cfg.txt") then + local dpp_cfg = mtkwifi.load_profile("/etc/dpp_cfg.txt") + dpp_cfg.allowed_role = "1" + mtkwifi.save_profile(dpp_cfg, "/etc/dpp_cfg.txt") + end + + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",mtkwifi.get_referer_url())) +end + +function save_easymesh_driver_profile(easymesh_cfgs) + local profiles = mtkwifi.search_dev_and_profile() + local detected_5g = false + -- Following EasyMesh settings must be written to all DAT files of Driver, + -- 1. MapEnable + -- 2. MAP_Turnkey + for _,profile in mtkwifi.__spairs(profiles, function(a,b) return string.upper(a) < string.upper(b) end) do + local driver_cfgs = mtkwifi.load_profile(profile) + driver_cfgs['MapMode'] = easymesh_cfgs['MapMode'] + if http.formvalue("TriBand") == "1" then + if detected_5g == false and mtkwifi.band(string.split(driver_cfgs.WirelessMode,";")[1]) == "5G" then + driver_cfgs['ChannelGrp'] = "0:0:1:1" + detected_5g = true + elseif detected_5g == true and mtkwifi.band(string.split(driver_cfgs.WirelessMode,";")[1]) == "5G" then + driver_cfgs['ChannelGrp'] = "1:1:0:0" + end + elseif http.formvalue("TriBand") == "2" then + if detected_5g == false and mtkwifi.band(string.split(driver_cfgs.WirelessMode,";")[1]) == "5G" then + driver_cfgs['ChannelGrp'] = "1:1:0:0" + detected_5g = true + elseif detected_5g == true and mtkwifi.band(string.split(driver_cfgs.WirelessMode,";")[1]) == "5G" then + driver_cfgs['ChannelGrp'] = "0:0:1:1" + end + end + if driver_cfgs['MapMode'] == "1" then + driver_cfgs['SREnable'] = "0" + driver_cfgs['SRMode'] = "0" + end + if easymesh_cfgs['MeshSREnable'] == "1" then + driver_cfgs['SREnable'] = "1" + driver_cfgs['SRMode'] = "1" + driver_cfgs['MapBalance'] = "1" + driver_cfgs['BSSColorValue'] = "254" + elseif easymesh_cfgs['MeshSREnable'] == "0" then + driver_cfgs['SREnable'] = "0" + driver_cfgs['SRMode'] = "0" + driver_cfgs['MapBalance'] = "0" + driver_cfgs['BSSColorValue'] = "255" + end + __mtkwifi_save_profile(driver_cfgs, profile, false) + end +end + +function map_cfg() + local easymesh_cfgs = mtkwifi.load_profile(mtkwifi.__write_easymesh_profile_path()) + assert(easymesh_cfgs) + + local easymesh_applied_path = mtkwifi.__profile_applied_settings_path(mtkwifi.__write_easymesh_profile_path()) + os.execute("cp -f "..mtkwifi.__write_easymesh_profile_path().." "..easymesh_applied_path) + + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + debug_write("map_cfg: Invalid value type for "..k..","..type(v)) + elseif string.byte(k) ~= string.byte("_") then + debug_write("map_cfg: Copying key:"..k..","..type(v)) + easymesh_cfgs[k] = v or "" + end + end + + local bands = mtkwifi.detect_triband() + if bands ~= 3 then + easymesh_cfgs['BhPriority5GH'] = easymesh_cfgs['BhPriority5GL'] + end + + save_easymesh_driver_profile(easymesh_cfgs) + mtkwifi.save_write_easymesh_profile(easymesh_cfgs) + + if http.formvalue("__apply") then + + if http.formvalue("__ChangeDeviceRole")=="changed" then + os.execute("wappctrl ra0 dpp dpp_reset_dpp_config_file") + end + + if mtkwifi.exists("/etc/dpp_cfg.txt") then + local dpp_cfg = mtkwifi.load_profile("/etc/dpp_cfg.txt") + if http.formvalue("DeviceRole")=="1" then + dpp_cfg.allowed_role="2" + elseif http.formvalue("DeviceRole")== "2" then + dpp_cfg.allowed_role="1" + elseif http.formvalue("DeviceRole")== "0" then + dpp_cfg.allowed_role="0" + end + mtkwifi.save_profile(dpp_cfg, "/etc/dpp_cfg.txt") + end + + if mtkwifi.exists("/usr/bin/map_restart.sh") then + mtkwifi.__run_in_child_env(exec_map_restart) + else + mtkwifi.__run_in_child_env(__mtkwifi_reload) + end + + local url_to_visit_after_reload = luci.dispatcher.build_url("admin", "mtk", "multi_ap") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "wifi", "loading",url_to_visit_after_reload)) + else + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "multi_ap")) + end +end + +function exec_map_restart() + if mtkwifi.exists("/usr/bin/map_restart.sh") then + os.execute("/usr/bin/map_restart.sh") + end +end + +function get_device_role() + local devRole = c_get_device_role() + -- Set ApCliEnable as "1" for Device with on-boarded ApCli interface to let + -- UI display connection information of ApCli interface on Wireless Overview web-page. + if tonumber(devRole.mapDevRole) == 2 then + local r = mtkwifi.get_easymesh_on_boarded_iface_info() + if r['status'] == "SUCCESS" then + for profile in string.gmatch(r['profile'],'(.-%.dat);') do + local cfgs = mtkwifi.load_profile(profile) + if cfgs.ApCliEnable ~= "1" or cfgs.ApCliEnable == nil then + cfgs.ApCliEnable = "1" + __mtkwifi_save_profile(cfgs, profile, true) + end + end + end + end + http.write_json(devRole) +end + +function trigger_uplink_ap_selection() + local r = c_trigger_uplink_ap_selection() + http.write_json(r) +end + +function trigger_mandate_steering_on_agent(sta_mac, target_bssid) + sta_mac = sta_mac:sub(1,17) + target_bssid = target_bssid:sub(1,17) + local r = c_trigger_mandate_steering_on_agent(sta_mac, target_bssid) + http.write_json(r) +end + +function trigger_back_haul_steering_on_agent(bh_mac, bh_target_bssid) + bh_mac = bh_mac:sub(1,17) + bh_target_bssid = bh_target_bssid:sub(1,17) + local r = c_trigger_back_haul_steering_on_agent(bh_mac, bh_target_bssid) + http.write_json(r) +end + +function trigger_wps_fh_agent(fh_bss_mac) + fh_bss_mac = fh_bss_mac:sub(1,17) + local r = c_trigger_wps_fh_agent(fh_bss_mac) + http.write_json(r) +end + +function trigger_multi_ap_on_boarding(ifmed) + assert(ifmed) + onboardingType = ifmed + debug_write("trigger_multi_ap_on_boarding: onboardingType:"..ifmed) + local r = c_trigger_multi_ap_on_boarding(ifmed) + http.write_json(r) +end + +function get_runtime_topology() + local r = c_get_runtime_topology() + http.write_json(r) +end + +function get_client_capabilities() + local r = c_get_client_capabilities() + http.write_json(r) +end + +function get_bh_connection_status() + local r = c_get_bh_connection_status() + http.write_json(r) +end + +function get_sta_steering_progress() + local r = {} + local fd = io.open("/tmp/sta_steer_progress","r") + if not fd then + r["status"] = "Failed to open /tmp/sta_steer_progress file in read mode!" + else + r["sta_steering_info"] = fd:read("*all") + fd:close() + r["status"] = "SUCCESS" + end + http.write_json(r) +end + +function get_al_mac(devRole) + local r = mtkwifi.get_easymesh_al_mac(devRole) + http.write_json(r) +end + +function apply_wifi_bh_priority(bhPriority2G, bhPriority5GL, bhPriority5GH) + assert(bhPriority2G) + assert(bhPriority5GL) + assert(bhPriority5GH) + debug_write("apply_wifi_bh_priority:BhPriority2G:"..bhPriority2G..", BhPriority5GL: "..bhPriority5GL..", BhPriority5GH: "..bhPriority5GH) + local r = c_apply_wifi_bh_priority(bhPriority2G, bhPriority5GL, bhPriority5GH) + if r.status == "SUCCESS" then + local read_easymesh_cfgs = mtkwifi.load_profile(mtkwifi.__read_easymesh_profile_path()) + read_easymesh_cfgs['BhPriority2G'] = bhPriority2G + read_easymesh_cfgs['BhPriority5GL'] = bhPriority5GL + read_easymesh_cfgs['BhPriority5GH'] = bhPriority5GH + mtkwifi.save_read_easymesh_profile(read_easymesh_cfgs) + + local write_easymesh_cfgs = mtkwifi.load_profile(mtkwifi.__write_easymesh_profile_path()) + write_easymesh_cfgs['BhPriority2G'] = bhPriority2G + write_easymesh_cfgs['BhPriority5GL'] = bhPriority5GL + write_easymesh_cfgs['BhPriority5GH'] = bhPriority5GH + mtkwifi.save_write_easymesh_profile(write_easymesh_cfgs) + end + http.write_json(r) +end + +function apply_ap_steer_rssi_th(rssi) + assert(rssi) + local r = c_apply_ap_steer_rssi_th(rssi) + if r.status == "SUCCESS" then + local easymesh_cfgs = mtkwifi.load_profile(mtkwifi.__read_easymesh_profile_path()) + if easymesh_cfgs['APSteerRssiTh'] ~= rssi then + easymesh_cfgs['APSteerRssiTh'] = rssi + mtkwifi.save_write_easymesh_profile(easymesh_cfgs) + end + local easymesh_mapd_cfgs = mtkwifi.load_profile(mtkwifi.__easymesh_mapd_profile_path()) + local mapd_rssi = tonumber(rssi) + 94 + if easymesh_mapd_cfgs['LowRSSIAPSteerEdge_RE'] ~= mapd_rssi then + easymesh_mapd_cfgs['LowRSSIAPSteerEdge_RE'] = mapd_rssi + mtkwifi.save_easymesh_mapd_profile(easymesh_mapd_cfgs) + end + end + http.write_json(r) +end + +function apply_force_ch_switch(agent_almac, channel1, channel2, channel3) + agent_almac = agent_almac:sub(1,17) + + if channel1 == nil then + channel1 = "" + end + + if channel2 == nil then + channel2 = "" + end + + if channel3 == nil then + channel3 = "" + end + + debug_write("apply_force_ch_switch() enter, agent_almac: "..agent_almac..", channel1:"..channel1..", channel2:"..channel2..", channe3:"..channel3) + local r = c_apply_force_ch_switch(agent_almac, channel1, channel2, channel3) + debug_write("apply_force_ch_switch() status: "..r.status) + http.write_json(r) +end + +function apply_user_preferred_channel(channel) + assert(channel) + debug_write("apply_user_preferred_channel() enter, channel:"..channel) + local r = c_apply_user_preferred_channel(channel) + debug_write("apply_user_preferred_channel() status: "..r.status) + http.write_json(r) +end + +function trigger_channel_planning_r2(band) + assert(band) + local r = c_trigger_channel_planning_r2(band) + http.write_json(r) +end + +function trigger_de_dump(almac) + assert(almac) + local r = c_trigger_de_dump(almac) + http.write_json(r) +end + +function get_data_element() + local r = c_get_data_element() + http.write_json(r) +end + +function trigger_channel_scan(almac) + assert(almac) + debug_write("trigger_channel_scan() enter, device AlMac:"..almac) + local r = c_trigger_channel_scan(almac) + debug_write("trigger_channel_scan() status: "..r.status) + http.write_json(r) +end + +function get_channel_stats() + local r = c_get_channel_stats() + http.write_json(r) +end + +function get_channel_planning_score() + local r = c_get_channel_planning_score() + http.write_json(r) +end + +function apply_channel_utilization_th(channelUtilTh2G, channelUtilTh5GL, channelUtilTh5GH) + assert(channelUtilTh2G) + assert(channelUtilTh5GL) + assert(channelUtilTh5GH) + local r = c_apply_channel_utilization_th(channelUtilTh2G, channelUtilTh5GL, channelUtilTh5GH) + if r.status == "SUCCESS" then + local easymesh_cfgs = mtkwifi.load_profile(mtkwifi.__read_easymesh_profile_path()) + if easymesh_cfgs['CUOverloadTh_2G'] ~= channelUtilTh2G or + easymesh_cfgs['CUOverloadTh_5G_L'] ~= channelUtilTh5GL or + easymesh_cfgs['CUOverloadTh_5G_H'] ~= channelUtilTh5GH then + easymesh_cfgs['CUOverloadTh_2G'] = channelUtilTh2G + easymesh_cfgs['CUOverloadTh_5G_L'] = channelUtilTh5GL + easymesh_cfgs['CUOverloadTh_5G_H'] = channelUtilTh5GH + mtkwifi.save_write_easymesh_profile(easymesh_cfgs) + end + local easymesh_mapd_cfgs = mtkwifi.load_profile(mtkwifi.__easymesh_mapd_profile_path()) + if easymesh_mapd_cfgs['CUOverloadTh_2G'] ~= channelUtilTh2G or + easymesh_mapd_cfgs['CUOverloadTh_5G_L'] ~= channelUtilTh5GL or + easymesh_mapd_cfgs['CUOverloadTh_5G_H'] ~= channelUtilTh5GH then + easymesh_mapd_cfgs['CUOverloadTh_2G'] = channelUtilTh2G + easymesh_mapd_cfgs['CUOverloadTh_5G_L'] = channelUtilTh5GL + easymesh_mapd_cfgs['CUOverloadTh_5G_H'] = channelUtilTh5GH + mtkwifi.save_easymesh_mapd_profile(easymesh_mapd_cfgs) + end + end + http.write_json(r) +end + +function get_sta_bh_interface() + local r = mtkwifi.get_easymesh_on_boarded_iface_info() + http.write_json(r) +end + +function get_ap_bh_inf_list() + local devs = mtkwifi.get_all_devs() + local r = c_get_ap_bh_inf_list() + if r.status == "SUCCESS" then + r['apBhInfListStr'] = "" + for mac in string.gmatch(r.macList, "(%x%x:%x%x:%x%x:%x%x:%x%x:%x%x);") do + for _, dev in ipairs(devs) do + local bssid_without_lf = dev.apcli and dev.apcli.mac_addr:upper():sub(1,17) or "" + if mac:upper() == bssid_without_lf then + r['apBhInfListStr'] = r['apBhInfListStr']..dev.apcli.vifname..';' + else + for _,vif in ipairs(dev.vifs) do + bssid_without_lf = vif.__bssid:upper():sub(1,17) + if mac:upper() == bssid_without_lf then + r['apBhInfListStr'] = r['apBhInfListStr']..vif.vifname..';' + end + end + end + end + end + end + http.write_json(r) +end + +function get_ap_fh_inf_list() + local devs = mtkwifi.get_all_devs() + local r = c_get_ap_fh_inf_list() + if r.status == "SUCCESS" then + r['apFhInfListStr'] = "" + for mac in string.gmatch(r.macList, "(%x%x:%x%x:%x%x:%x%x:%x%x:%x%x);") do + for _, dev in ipairs(devs) do + local bssid_without_lf = dev.apcli and dev.apcli.mac_addr:upper():sub(1,17) or "" + if mac:upper() == bssid_without_lf then + r['apFhInfListStr'] = r['apFhInfListStr']..dev.apcli.vifname..';' + else + for _,vif in ipairs(dev.vifs) do + bssid_without_lf = vif.__bssid:upper():sub(1,17) + if mac:upper() == bssid_without_lf then + r['apFhInfListStr'] = r['apFhInfListStr']..vif.vifname..';' + end + end + end + end + end + end + http.write_json(r) +end + +function validate_easymesh_bss(r, cfgs, alMac, band) + assert(type(r) == 'table') + assert(type(cfgs) == 'table') + assert(type(alMac) == 'string') + assert(type(band) == 'string') + if not cfgs[alMac] then + r['status'] = 'SUCCESS' + elseif not cfgs[alMac][band] then + r['status'] = 'SUCCESS' + else + local numBss = mtkwifi.get_table_length(cfgs[alMac][band]) + if numBss >= 4 then + r['status'] = 'No more BSS could be added!' + else + r['status'] = 'SUCCESS' + end + end +end + +function validate_add_easymesh_bss_req(alMac, band) + local r = {} + local cfgs = mtkwifi.load_easymesh_bss_cfgs() + if type(alMac) ~= 'string' then + r["status"]= "Invalid AL-MAC Type "..type(alMac).." !" + elseif type(band) ~= 'string' then + r["status"]= "Invalid Band Type "..type(band).." !" + else + if type(cfgs) ~= "table" or next(cfgs) == nil then + cfgs = {} + cfgs['wildCardAlMacCfgs'] = {} + cfgs['distinctAlMacCfgs'] = {} + end + if alMac == 'FF:FF:FF:FF:FF:FF' then + validate_easymesh_bss(r, cfgs['wildCardAlMacCfgs'], alMac, band) + else + validate_easymesh_bss(r, cfgs['distinctAlMacCfgs'], alMac, band) + end + end + if type(r) ~= 'table' or next(r) == nil then + r['status'] = "Unexpected Exception in validate_easymesh_bss()!" + end + http.write_json(r) +end + +function apply_easymesh_bss_cfg(isLocal) + local r = c_apply_bss_config_renew() + if r['status'] == 'SUCCESS' then + local easymesh_bss_cfg_applied_path = mtkwifi.__profile_applied_settings_path(mtkwifi.__easymesh_bss_cfgs_path()) + os.execute("cp -f "..mtkwifi.__easymesh_bss_cfgs_path().." "..easymesh_bss_cfg_applied_path) + end + if isLocal then + return r + else + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "multi_ap", "easymesh_bss_config_renew")) + end +end + +function get_easymesh_bss_index(bssInfoTbl, bssInfoInp) + assert(type(bssInfoTbl) == 'table') + assert(type(bssInfoInp) == 'table') + for bssIdx, bssInfo in pairs(bssInfoTbl) do + debug_write("get SSID from wts_bss_info_config = "..bssInfo['ssid']) + bssInfoInp['defPCP'] = "N/A" + bssInfoInp['primVlan'] = "N/A" + if bssInfo['ssid'] == bssInfoInp['ssid'] and + bssInfo['authMode'] == bssInfoInp['authMode'] and + bssInfo['encType'] == bssInfoInp['encType'] and + bssInfo['passPhrase'] == bssInfoInp['passPhrase'] and + bssInfo['isBhBssSupported'] == bssInfoInp['isBhBssSupported'] and + bssInfo['isFhBssSupported'] == bssInfoInp['isFhBssSupported'] and + bssInfo['isHidden'] == bssInfoInp['isHidden'] and + bssInfo['fhVlanId'] == bssInfoInp['fhVlanId'] and + bssInfo['primVlan'] == bssInfoInp['primVlan'] and + bssInfo['defPCP'] == bssInfoInp['defPCP'] then + return bssIdx + end + end + return nil +end + +function update_easymesh_bss(cfgs, bssInfoInp, isEdit) + assert(type(cfgs) == 'table') + assert(type(bssInfoInp) == 'table') + assert(type(isEdit) == 'string') + if not cfgs[bssInfoInp['alMac']] then + cfgs[bssInfoInp['alMac']] = {} + end + if not cfgs[bssInfoInp['alMac']][bssInfoInp['band']] then + cfgs[bssInfoInp['alMac']][bssInfoInp['band']] = {} + end + local bssInfoTbl = cfgs[bssInfoInp['alMac']][bssInfoInp['band']] + local bssInfoIdx + if isEdit == "1" then + local editBssInfo = {} + local tmpEditSSID = http.formvalue('__EDIT_SSID'):gsub("\\", "\\\\") + editBssInfo['ssid'] = tmpEditSSID:gsub("%s+","\\ ") + debug_write("get edited SSID from UI = "..editBssInfo['ssid']) + editBssInfo['authMode'] = http.formvalue('__EDIT_AUTH_MODE') + editBssInfo['encType'] = http.formvalue('__EDIT_ENCRYPTION_TYPE') + local tmpEditPassPhrase = http.formvalue('__EDIT_PASS_PHRASE'):gsub("\\", "\\\\") + editBssInfo['passPhrase'] = tmpEditPassPhrase:gsub("%s+","\\ ") + editBssInfo['isBhBssSupported'] = http.formvalue('__EDIT_BH_SUPPORT') + editBssInfo['isFhBssSupported'] = http.formvalue('__EDIT_FH_SUPPORT') + editBssInfo['isHidden'] = http.formvalue('__EDIT_IS_SSID_HIDDEN') + editBssInfo['fhVlanId'] = http.formvalue('__EDIT_FH_VLAN_ID') + editBssInfo['primVlan'] = http.formvalue('__EDIT_PRIM_VLAN') + editBssInfo['defPCP'] = http.formvalue('__EDIT_DEF_PCP') + bssInfoIdx = get_easymesh_bss_index(bssInfoTbl, editBssInfo) + assert(bssInfoIdx) + assert(type(bssInfoTbl[bssInfoIdx]) == 'table') + else + bssInfoIdx = mtkwifi.get_table_length(bssInfoTbl) + 1 + bssInfoTbl[bssInfoIdx] = {} + end + local bssInfo = bssInfoTbl[bssInfoIdx] + bssInfo['ssid'] = bssInfoInp['ssid'] + debug_write("final SSID write to wts_bss_info_config = "..bssInfo['ssid']) + bssInfo['authMode'] = bssInfoInp['authMode'] + bssInfo['encType'] = bssInfoInp['encType'] + bssInfo['passPhrase'] = bssInfoInp['passPhrase'] and bssInfoInp['passPhrase'] ~= '' and bssInfoInp['passPhrase'] or 'fixture1' + bssInfo['isBhBssSupported'] = bssInfoInp['isBhBssSupported'] + bssInfo['isFhBssSupported'] = bssInfoInp['isFhBssSupported'] + bssInfo['isHidden'] = bssInfoInp['isHidden'] + bssInfo['fhVlanId'] = bssInfoInp['fhVlanId'] + bssInfo['primVlan'] = bssInfoInp['primVlan'] + bssInfo['defPCP'] = bssInfoInp['defPCP'] + +end + +function easymesh_bss_cfg() + local cfgs = mtkwifi.load_easymesh_bss_cfgs() + + local bssInfoInp = {} + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + debug_write("easymesh_bss_cfg: Input BSSINFO are of incorrect type!",k,v) + elseif string.byte(k) ~= string.byte("_") then + bssInfoInp[k] = v + end + end + + if bssInfoInp['primVlan'] ~= "N/A" and bssInfoInp['defPCP'] ~= "N/A" then + for alMac,alMacTbl in pairs(cfgs['wildCardAlMacCfgs']) do + for band,bssInfoTbl in pairs(alMacTbl) do + for _,bssInfo in pairs(bssInfoTbl) do + bssInfo['primVlan'] = "N/A" + bssInfo['defPCP'] = "N/A" + end + end + end + + for alMac,alMacTbl in pairs(cfgs['distinctAlMacCfgs']) do + for band,bssInfoTbl in pairs(alMacTbl) do + for _,bssInfo in pairs(bssInfoTbl) do + bssInfo['primVlan'] = "N/A" + bssInfo['defPCP'] = "N/A" + end + end + end + end + + debug_write("original SSID which user entered = "..bssInfoInp['ssid']) + local tmpSSID = bssInfoInp['ssid']:gsub("\\", "\\\\") + bssInfoInp['ssid'] = tmpSSID:gsub("%s+","\\ ") + debug_write("get SSID from UI = "..bssInfoInp['ssid']) + local tmpPassPhrase = bssInfoInp['passPhrase']:gsub("\\", "\\\\") + bssInfoInp['passPhrase'] = tmpPassPhrase:gsub("%s+","\\ ") + if type(cfgs) ~= "table" or next(cfgs) == nil then + cfgs = {} + cfgs['wildCardAlMacCfgs'] = {} + cfgs['distinctAlMacCfgs'] = {} + end + if bssInfoInp['alMac'] == 'FF:FF:FF:FF:FF:FF' then + update_easymesh_bss(cfgs['wildCardAlMacCfgs'], bssInfoInp, http.formvalue('__IS_EDIT')) + else + update_easymesh_bss(cfgs['distinctAlMacCfgs'], bssInfoInp, http.formvalue('__IS_EDIT')) + end + mtkwifi.save_easymesh_bss_cfgs(cfgs) + if http.formvalue("__apply") then + apply_easymesh_bss_cfg(true) + end + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "multi_ap", "easymesh_bss_config_renew")) +end + +function remove_easymesh_bss(r,cfgs,bssInfoInp) + assert(type(r) == 'table') + assert(type(cfgs) == 'table') + assert(type(bssInfoInp) == 'table') + for alMac,alMacTbl in pairs(cfgs) do + if alMac == bssInfoInp['alMac'] then + assert(type(alMacTbl) == 'table') + for band,bssInfoTbl in pairs(alMacTbl) do + if bssInfoInp['primVlan'] ~= "N/A" and bssInfoInp['defPCP'] ~= "N/A" then + for _,bssInfo in pairs(bssInfoTbl) do + bssInfo['primVlan'] = "N/A" + bssInfo['defPCP'] = "N/A" + end + end + if band == bssInfoInp['band'] then + assert(type(bssInfoTbl) == 'table') + local bssIdx = get_easymesh_bss_index(bssInfoTbl, bssInfoInp) + if bssIdx then + local alMacTblLen = mtkwifi.get_table_length(alMacTbl) + local bssInfoTblLen = mtkwifi.get_table_length(bssInfoTbl) + if bssInfoTblLen == 1 then + cfgs[alMac][band] = nil + if alMacTblLen == 1 then + cfgs[alMac] = nil + end + else + table.remove(cfgs[alMac][band], tonumber(bssIdx)) + end + r['status'] = 'SUCCESS' + else + r['status'] = 'ERROR: BSSINFO does not exist!' + end + break + end + end + if next(r) == nil then + r['status'] = 'ERROR: BAND does not exist!' + end + break + end + end + if next(r) == nil then + r['status'] = 'ERROR: AL-MAC does not exist!' + end +end + +function remove_easymesh_bss_cfg_req() + local r = {} + local cfgs = mtkwifi.load_easymesh_bss_cfgs() + if type(cfgs) ~= "table" or next(cfgs) == nil then + r["status"]= mtkwifi.__easymesh_bss_cfgs_path().." file not found!" + else + local bssInfoInp = {} + for k,v in pairs(http.formvalue()) do + if type(v) ~= type("") and type(v) ~= type(0) then + r["status"]= "Input BSSINFO are of incorrect type!" + break + elseif string.byte(k) ~= string.byte("_") then + bssInfoInp[k] = v + end + end + local tmpSSID = bssInfoInp['ssid']:gsub("\\", "\\\\") + bssInfoInp['ssid'] = tmpSSID:gsub("%s+","\\ ") + local tmpPassPhrase = bssInfoInp['passPhrase']:gsub("\\", "\\\\") + bssInfoInp['passPhrase'] = tmpPassPhrase:gsub("%s+","\\ ") + if next(r) == nil then + if bssInfoInp['alMac'] == 'FF:FF:FF:FF:FF:FF' then + remove_easymesh_bss(r, cfgs['wildCardAlMacCfgs'], bssInfoInp) + else + remove_easymesh_bss(r, cfgs['distinctAlMacCfgs'], bssInfoInp) + end + end + end + if type(r) ~= 'table' or next(r) == nil then + r['status'] = "Unexpected Exception in remove_easymesh_bss()!" + else + mtkwifi.save_easymesh_bss_cfgs(cfgs) + r = apply_easymesh_bss_cfg(true) + end + http.write_json(r) +end + +function get_user_preferred_channel() + local r = c_get_user_preferred_channel() + http.write_json(r) +end + +function get_sp_rule_list() + local r = c_get_sp_rule_list() + http.write_json(r) +end + +function del_sp_rule(index) + if index == nil then + index = "" + end + local r = c_del_sp_rule(index) + http.write_json(r) +end + +function sp_rule_reorder(index1, index2) + local r = c_sp_rule_reorder(index1, index2) + http.write_json(r) +end + +function sp_rule_move(index, action) + local r = c_sp_rule_move(index, action) + http.write_json(r) +end + +function sp_rule_add(str_rule) + str_rule = string.gsub(str_rule, "] ", "]+") + local r = c_sp_rule_add(str_rule) + http.write_json(r) +end + +function sp_config_done() + local r = c_sp_config_done() + http.write_json(r) +end + +function submit_dpp_uri() + uri = http.formvalue("uri") + os.execute("wappctrl ra0 dpp dpp_qr_code ".."\""..uri.."\"") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "multi_ap")) +end + +function start_dpp_onboarding() + os.execute("wappctrl ra0 dpp dpp_start") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "multi_ap")) +end + +function generate_dpp_uri() + os.execute("wappctrl ra0 dpp dpp_bootstrap_gen type=qrcode") + luci.http.redirect(luci.dispatcher.build_url("admin", "mtk", "multi_ap")) +end + +function retrive_dpp_uri() + local result = mtkwifi.read_pipe(tostring("mapd_cli /tmp/mapd_ctrl get_dpp_uri").." 2>&1") + result = result:gsub("<", "<") + http.write(tostring(result)) +end \ No newline at end of file diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/mtkwifi.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/mtkwifi.luac new file mode 100644 index 000000000000..9d611ba45bde Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/controller/mtkwifi.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/debug.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/debug.lua new file mode 100644 index 000000000000..8ff1bb69818b --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/debug.lua @@ -0,0 +1,37 @@ +local debug = require "debug" +local io = require "io" +local collectgarbage, floor = collectgarbage, math.floor + +module "luci.debug" +__file__ = debug.getinfo(1, 'S').source:sub(2) + +-- Enables the memory tracer with given flags and returns a function to disable the tracer again +function trap_memtrace(flags, dest) + flags = flags or "clr" + local tracefile = io.open(dest or "/tmp/memtrace", "w") + local peak = 0 + + local function trap(what, line) + local info = debug.getinfo(2, "Sn") + local size = floor(collectgarbage("count")) + if size > peak then + peak = size + end + if tracefile then + tracefile:write( + "[", what, "] ", info.source, ":", (line or "?"), "\t", + (info.namewhat or ""), "\t", + (info.name or ""), "\t", + size, " (", peak, ")\n" + ) + end + end + + debug.sethook(trap, flags) + + return function() + debug.sethook() + tracefile:close() + end +end + diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/debug.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/debug.luac new file mode 100644 index 000000000000..b73fe8727211 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/debug.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/dispatcher.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/dispatcher.lua new file mode 100644 index 000000000000..bd1b112f60cd --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/dispatcher.lua @@ -0,0 +1,1532 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2008-2015 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. + +local fs = require "nixio.fs" +local sys = require "luci.sys" +local util = require "luci.util" +local xml = require "luci.xml" +local http = require "luci.http" +local nixio = require "nixio", require "nixio.util" + +module("luci.dispatcher", package.seeall) +context = util.threadlocal() +uci = require "luci.model.uci" +i18n = require "luci.i18n" +_M.fs = fs + +-- Index table +local index = nil + +local function check_fs_depends(spec) + local fs = require "nixio.fs" + + for path, kind in pairs(spec) do + if kind == "directory" then + local empty = true + for entry in (fs.dir(path) or function() end) do + empty = false + break + end + if empty then + return false + end + elseif kind == "executable" then + if fs.stat(path, "type") ~= "reg" or not fs.access(path, "x") then + return false + end + elseif kind == "file" then + if fs.stat(path, "type") ~= "reg" then + return false + end + end + end + + return true +end + +local function check_uci_depends_options(conf, s, opts) + local uci = require "luci.model.uci" + + if type(opts) == "string" then + return (s[".type"] == opts) + elseif opts == true then + for option, value in pairs(s) do + if option:byte(1) ~= 46 then + return true + end + end + elseif type(opts) == "table" then + for option, value in pairs(opts) do + local sval = s[option] + if type(sval) == "table" then + local found = false + for _, v in ipairs(sval) do + if v == value then + found = true + break + end + end + if not found then + return false + end + elseif value == true then + if sval == nil then + return false + end + else + if sval ~= value then + return false + end + end + end + end + + return true +end + +local function check_uci_depends_section(conf, sect) + local uci = require "luci.model.uci" + + for section, options in pairs(sect) do + local stype = section:match("^@([A-Za-z0-9_%-]+)$") + if stype then + local found = false + uci:foreach(conf, stype, function(s) + if check_uci_depends_options(conf, s, options) then + found = true + return false + end + end) + if not found then + return false + end + else + local s = uci:get_all(conf, section) + if not s or not check_uci_depends_options(conf, s, options) then + return false + end + end + end + + return true +end + +local function check_uci_depends(conf) + local uci = require "luci.model.uci" + + for config, values in pairs(conf) do + if values == true then + local found = false + uci:foreach(config, nil, function(s) + found = true + return false + end) + if not found then + return false + end + elseif type(values) == "table" then + if not check_uci_depends_section(config, values) then + return false + end + end + end + + return true +end + +local function check_acl_depends(require_groups, groups) + if type(require_groups) == "table" and #require_groups > 0 then + local writable = false + + for _, group in ipairs(require_groups) do + local read = false + local write = false + if type(groups) == "table" and type(groups[group]) == "table" then + for _, perm in ipairs(groups[group]) do + if perm == "read" then + read = true + elseif perm == "write" then + write = true + end + end + end + if not read and not write then + return nil + elseif write then + writable = true + end + end + + return writable + end + + return true +end + +local function check_depends(spec) + if type(spec.depends) ~= "table" then + return true + end + + if type(spec.depends.fs) == "table" then + local satisfied = false + local alternatives = (#spec.depends.fs > 0) and spec.depends.fs or { spec.depends.fs } + for _, alternative in ipairs(alternatives) do + if check_fs_depends(alternative) then + satisfied = true + break + end + end + if not satisfied then + return false + end + end + + if type(spec.depends.uci) == "table" then + local satisfied = false + local alternatives = (#spec.depends.uci > 0) and spec.depends.uci or { spec.depends.uci } + for _, alternative in ipairs(alternatives) do + if check_uci_depends(alternative) then + satisfied = true + break + end + end + if not satisfied then + return false + end + end + + return true +end + +local function target_to_json(target, module) + local action + + if target.type == "call" then + action = { + ["type"] = "call", + ["module"] = module, + ["function"] = target.name, + ["parameters"] = target.argv + } + elseif target.type == "view" then + action = { + ["type"] = "view", + ["path"] = target.view + } + elseif target.type == "template" then + action = { + ["type"] = "template", + ["path"] = target.view + } + elseif target.type == "cbi" then + action = { + ["type"] = "cbi", + ["path"] = target.model, + ["config"] = target.config + } + elseif target.type == "form" then + action = { + ["type"] = "form", + ["path"] = target.model + } + elseif target.type == "firstchild" then + action = { + ["type"] = "firstchild" + } + elseif target.type == "firstnode" then + action = { + ["type"] = "firstchild", + ["recurse"] = true + } + elseif target.type == "arcombine" then + if type(target.targets) == "table" then + action = { + ["type"] = "arcombine", + ["targets"] = { + target_to_json(target.targets[1], module), + target_to_json(target.targets[2], module) + } + } + end + elseif target.type == "alias" then + action = { + ["type"] = "alias", + ["path"] = table.concat(target.req, "/") + } + elseif target.type == "rewrite" then + action = { + ["type"] = "rewrite", + ["path"] = table.concat(target.req, "/"), + ["remove"] = target.n + } + end + + if target.post and action then + action.post = target.post + end + + return action +end + +local function tree_to_json(node, json) + local fs = require "nixio.fs" + local util = require "luci.util" + + if type(node.nodes) == "table" then + for subname, subnode in pairs(node.nodes) do + local spec = { + title = xml.striptags(subnode.title), + order = subnode.order + } + + if subnode.leaf then + spec.wildcard = true + end + + if subnode.cors then + spec.cors = true + end + + if subnode.setuser then + spec.setuser = subnode.setuser + end + + if subnode.setgroup then + spec.setgroup = subnode.setgroup + end + + if type(subnode.target) == "table" then + spec.action = target_to_json(subnode.target, subnode.module) + end + + if type(subnode.file_depends) == "table" then + for _, v in ipairs(subnode.file_depends) do + spec.depends = spec.depends or {} + spec.depends.fs = spec.depends.fs or {} + + local ft = fs.stat(v, "type") + if ft == "dir" then + spec.depends.fs[v] = "directory" + elseif v:match("/s?bin/") then + spec.depends.fs[v] = "executable" + else + spec.depends.fs[v] = "file" + end + end + end + + if type(subnode.uci_depends) == "table" then + for k, v in pairs(subnode.uci_depends) do + spec.depends = spec.depends or {} + spec.depends.uci = spec.depends.uci or {} + spec.depends.uci[k] = v + end + end + + if type(subnode.acl_depends) == "table" then + for _, acl in ipairs(subnode.acl_depends) do + spec.depends = spec.depends or {} + spec.depends.acl = spec.depends.acl or {} + spec.depends.acl[#spec.depends.acl + 1] = acl + end + end + + if (subnode.sysauth_authenticator ~= nil) or + (subnode.sysauth ~= nil and subnode.sysauth ~= false) + then + if subnode.sysauth_authenticator == "htmlauth" then + spec.auth = { + login = true, + methods = { "cookie:sysauth" } + } + elseif subname == "rpc" and subnode.module == "luci.controller.rpc" then + spec.auth = { + login = false, + methods = { "query:auth", "cookie:sysauth" } + } + elseif subnode.module == "luci.controller.admin.uci" then + spec.auth = { + login = false, + methods = { "param:sid" } + } + end + elseif subnode.sysauth == false then + spec.auth = {} + end + + if not spec.action then + spec.title = nil + end + + spec.satisfied = check_depends(spec) + json.children = json.children or {} + json.children[subname] = tree_to_json(subnode, spec) + end + end + + return json +end + +function build_url(...) + local path = {...} + local url = { http.getenv("SCRIPT_NAME") or "" } + + local p + for _, p in ipairs(path) do + if p:match("^[a-zA-Z0-9_%-%.%%/,;]+$") then + url[#url+1] = "/" + url[#url+1] = p + end + end + + if #path == 0 then + url[#url+1] = "/" + end + + return table.concat(url, "") +end + + +function error404(message) + http.status(404, "Not Found") + message = message or "Not Found" + + local function render() + local template = require "luci.template" + template.render("error404", {message=message}) + end + + if not util.copcall(render) then + http.prepare_content("text/plain") + http.write(message) + end + + return false +end + +function error500(message) + util.perror(message) + if not context.template_header_sent then + http.status(500, "Internal Server Error") + http.prepare_content("text/plain") + http.write(message) + else + require("luci.template") + if not util.copcall(luci.template.render, "error500", {message=message}) then + http.prepare_content("text/plain") + http.write(message) + end + end + return false +end + +local function determine_request_language() + local conf = require "luci.config" + assert(conf.main, "/etc/config/luci seems to be corrupt, unable to find section 'main'") + + local lang = conf.main.lang or "auto" + if lang == "auto" then + local aclang = http.getenv("HTTP_ACCEPT_LANGUAGE") or "" + for aclang in aclang:gmatch("[%w_-]+") do + local country, culture = aclang:match("^([a-z][a-z])[_-]([a-zA-Z][a-zA-Z])$") + if country and culture then + local cc = "%s_%s" %{ country, culture:lower() } + if conf.languages[cc] then + lang = cc + break + elseif conf.languages[country] then + lang = country + break + end + elseif conf.languages[aclang] then + lang = aclang + break + end + end + end + + if lang == "auto" then + lang = i18n.default + end + + i18n.setlanguage(lang) +end + +function httpdispatch(request, prefix) + http.context.request = request + + local r = {} + context.request = r + + local pathinfo = http.urldecode(request:getenv("PATH_INFO") or "", true) + + if prefix then + for _, node in ipairs(prefix) do + r[#r+1] = node + end + end + + local node + for node in pathinfo:gmatch("[^/%z]+") do + r[#r+1] = node + end + + determine_request_language() + + local stat, err = util.coxpcall(function() + dispatch(context.request) + end, error500) + + http.close() + + --context._disable_memtrace() +end + +local function require_post_security(target, args) + if type(target) == "table" and target.type == "arcombine" and type(target.targets) == "table" then + return require_post_security((type(args) == "table" and #args > 0) and target.targets[2] or target.targets[1], args) + end + + if type(target) == "table" then + if type(target.post) == "table" then + local param_name, required_val, request_val + + for param_name, required_val in pairs(target.post) do + request_val = http.formvalue(param_name) + + if (type(required_val) == "string" and + request_val ~= required_val) or + (required_val == true and request_val == nil) + then + return false + end + end + + return true + end + + return (target.post == true) + end + + return false +end + +function test_post_security() + if http.getenv("REQUEST_METHOD") ~= "POST" then + http.status(405, "Method Not Allowed") + http.header("Allow", "POST") + return false + end + + if http.formvalue("token") ~= context.authtoken then + http.status(403, "Forbidden") + luci.template.render("csrftoken") + return false + end + + return true +end + +local function session_retrieve(sid, allowed_users) + local sdat = util.ubus("session", "get", { ubus_rpc_session = sid }) + local sacl = util.ubus("session", "access", { ubus_rpc_session = sid }) + + if type(sdat) == "table" and + type(sdat.values) == "table" and + type(sdat.values.token) == "string" and + (not allowed_users or + util.contains(allowed_users, sdat.values.username)) + then + uci:set_session_id(sid) + return sid, sdat.values, type(sacl) == "table" and sacl or {} + end + + return nil, nil, nil +end + +local function session_setup(user, pass) + local login = util.ubus("session", "login", { + username = user, + password = pass, + timeout = tonumber(luci.config.sauth.sessiontime) + }) + + local rp = context.requestpath + and table.concat(context.requestpath, "/") or "" + + if type(login) == "table" and + type(login.ubus_rpc_session) == "string" + then + util.ubus("session", "set", { + ubus_rpc_session = login.ubus_rpc_session, + values = { token = sys.uniqueid(16) } + }) + nixio.syslog("info", tostring("luci: accepted login on /%s for %s from %s\n" + %{ rp, user or "?", http.getenv("REMOTE_ADDR") or "?" })) + + return session_retrieve(login.ubus_rpc_session) + end + nixio.syslog("info", tostring("luci: failed login on /%s for %s from %s\n" + %{ rp, user or "?", http.getenv("REMOTE_ADDR") or "?" })) +end + +local function check_authentication(method) + local auth_type, auth_param = method:match("^(%w+):(.+)$") + local sid, sdat + + if auth_type == "cookie" then + sid = http.getcookie(auth_param) + elseif auth_type == "param" then + sid = http.formvalue(auth_param) + elseif auth_type == "query" then + sid = http.formvalue(auth_param, true) + end + + return session_retrieve(sid) +end + +local function get_children(node) + local children = {} + + if not node.wildcard and type(node.children) == "table" then + for name, child in pairs(node.children) do + children[#children+1] = { + name = name, + node = child, + order = child.order or 1000 + } + end + + table.sort(children, function(a, b) + if a.order == b.order then + return a.name < b.name + else + return a.order < b.order + end + end) + end + + return children +end + +local function find_subnode(root, prefix, recurse, descended) + local children = get_children(root) + + if #children > 0 and (not descended or recurse) then + local sub_path = { unpack(prefix) } + + if recurse == false then + recurse = nil + end + + for _, child in ipairs(children) do + sub_path[#prefix+1] = child.name + + local res_path = find_subnode(child.node, sub_path, recurse, true) + + if res_path then + return res_path + end + end + end + + if descended then + if not recurse or + root.action.type == "cbi" or + root.action.type == "form" or + root.action.type == "view" or + root.action.type == "template" or + root.action.type == "arcombine" + then + return prefix + end + end +end + +local function merge_trees(node_a, node_b) + for k, v in pairs(node_b) do + if k == "children" then + node_a.children = node_a.children or {} + + for name, spec in pairs(v) do + node_a.children[name] = merge_trees(node_a.children[name] or {}, spec) + end + else + node_a[k] = v + end + end + + if type(node_a.action) == "table" and + node_a.action.type == "firstchild" and + node_a.children == nil + then + node_a.satisfied = false + end + + return node_a +end + +local function apply_tree_acls(node, acl) + if type(node.children) == "table" then + for _, child in pairs(node.children) do + apply_tree_acls(child, acl) + end + end + + local perm + if type(node.depends) == "table" then + perm = check_acl_depends(node.depends.acl, acl["access-group"]) + else + perm = true + end + + if perm == nil then + node.satisfied = false + elseif perm == false then + node.readonly = true + end +end + +function menu_json(acl) + local tree = context.tree or createtree() + local lua_tree = tree_to_json(tree, { + action = { + ["type"] = "firstchild", + ["recurse"] = true + } + }) + + local json_tree = createtree_json() + local menu_tree = merge_trees(lua_tree, json_tree) + + if acl then + apply_tree_acls(menu_tree, acl) + end + + return menu_tree +end + +local function init_template_engine(ctx) + local tpl = require "luci.template" + local media = luci.config.main.mediaurlbase + + if not pcall(tpl.Template, "themes/%s/header" % fs.basename(media)) then + media = nil + for name, theme in pairs(luci.config.themes) do + if name:sub(1,1) ~= "." and pcall(tpl.Template, + "themes/%s/header" % fs.basename(theme)) then + media = theme + end + end + assert(media, "No valid theme found") + end + + local function _ifattr(cond, key, val, noescape) + if cond then + local env = getfenv(3) + local scope = (type(env.self) == "table") and env.self + if type(val) == "table" then + if not next(val) then + return '' + else + val = util.serialize_json(val) + end + end + + val = tostring(val or + (type(env[key]) ~= "function" and env[key]) or + (scope and type(scope[key]) ~= "function" and scope[key]) or "") + + if noescape ~= true then + val = xml.pcdata(val) + end + + return string.format(' %s="%s"', tostring(key), val) + else + return '' + end + end + + tpl.context.viewns = setmetatable({ + write = http.write; + include = function(name) tpl.Template(name):render(getfenv(2)) end; + translate = i18n.translate; + translatef = i18n.translatef; + export = function(k, v) if tpl.context.viewns[k] == nil then tpl.context.viewns[k] = v end end; + striptags = xml.striptags; + pcdata = xml.pcdata; + media = media; + theme = fs.basename(media); + resource = luci.config.main.resourcebase; + ifattr = function(...) return _ifattr(...) end; + attr = function(...) return _ifattr(true, ...) end; + url = build_url; + }, {__index=function(tbl, key) + if key == "controller" then + return build_url() + elseif key == "REQUEST_URI" then + return build_url(unpack(ctx.requestpath)) + elseif key == "FULL_REQUEST_URI" then + local url = { http.getenv("SCRIPT_NAME") or "", http.getenv("PATH_INFO") } + local query = http.getenv("QUERY_STRING") + if query and #query > 0 then + url[#url+1] = "?" + url[#url+1] = query + end + return table.concat(url, "") + elseif key == "token" then + return ctx.authtoken + else + return rawget(tbl, key) or _G[key] + end + end}) + + return tpl +end + +function dispatch(request) + --context._disable_memtrace = require "luci.debug".trap_memtrace("l") + local ctx = context + + local auth, cors, suid, sgid + local menu = menu_json() + local page = menu + + local requested_path_full = {} + local requested_path_node = {} + local requested_path_args = {} + + local required_path_acls = {} + + for i, s in ipairs(request) do + if type(page.children) ~= "table" or not page.children[s] then + page = nil + break + end + + if not page.children[s].satisfied then + page = nil + break + end + + page = page.children[s] + auth = page.auth or auth + cors = page.cors or cors + suid = page.setuser or suid + sgid = page.setgroup or sgid + + if type(page.depends) == "table" and type(page.depends.acl) == "table" then + for _, group in ipairs(page.depends.acl) do + local found = false + for _, item in ipairs(required_path_acls) do + if item == group then + found = true + break + end + end + if not found then + required_path_acls[#required_path_acls + 1] = group + end + end + end + + requested_path_full[i] = s + requested_path_node[i] = s + + if page.wildcard then + for j = i + 1, #request do + requested_path_args[j - i] = request[j] + requested_path_full[j] = request[j] + end + break + end + end + + local tpl = init_template_engine(ctx) + + ctx.args = requested_path_args + ctx.path = requested_path_node + ctx.dispatched = page + + ctx.requestpath = ctx.requestpath or requested_path_full + ctx.requestargs = ctx.requestargs or requested_path_args + ctx.requested = ctx.requested or page + + if type(auth) == "table" and type(auth.methods) == "table" and #auth.methods > 0 then + local sid, sdat, sacl + for _, method in ipairs(auth.methods) do + sid, sdat, sacl = check_authentication(method) + + if sid and sdat and sacl then + break + end + end + + if not (sid and sdat and sacl) and auth.login then + local user = http.getenv("HTTP_AUTH_USER") + local pass = http.getenv("HTTP_AUTH_PASS") + + if user == nil and pass == nil then + user = http.formvalue("luci_username") + pass = http.formvalue("luci_password") + end + + if user and pass then + sid, sdat, sacl = session_setup(user, pass) + end + + if not sid then + context.path = {} + + http.status(403, "Forbidden") + http.header("X-LuCI-Login-Required", "yes") + + local scope = { duser = "root", fuser = user } + local ok, res = util.copcall(tpl.render_string, [[<% include("themes/" .. theme .. "/sysauth") %>]], scope) + if ok then + return res + end + return tpl.render("sysauth", scope) + end + + http.header("Set-Cookie", 'sysauth=%s; path=%s; SameSite=Strict; HttpOnly%s' %{ + sid, build_url(), http.getenv("HTTPS") == "on" and "; secure" or "" + }) + + http.redirect(build_url(unpack(ctx.requestpath))) + return + end + + if not sid or not sdat or not sacl then + http.status(403, "Forbidden") + http.header("X-LuCI-Login-Required", "yes") + return + end + + ctx.authsession = sid + ctx.authtoken = sdat.token + ctx.authuser = sdat.username + ctx.authacl = sacl + end + + if #required_path_acls > 0 then + local perm = check_acl_depends(required_path_acls, ctx.authacl and ctx.authacl["access-group"]) + if perm == nil then + http.status(403, "Forbidden") + return + end + + if page then + page.readonly = not perm + end + end + + local action = (page and type(page.action) == "table") and page.action or {} + + if action.type == "arcombine" then + action = (#requested_path_args > 0) and action.targets[2] or action.targets[1] + end + + if cors and http.getenv("REQUEST_METHOD") == "OPTIONS" then + luci.http.status(200, "OK") + luci.http.header("Access-Control-Allow-Origin", http.getenv("HTTP_ORIGIN") or "*") + luci.http.header("Access-Control-Allow-Methods", "GET, POST, OPTIONS") + return + end + + if require_post_security(action) then + if not test_post_security() then + return + end + end + + if sgid then + sys.process.setgroup(sgid) + end + + if suid then + sys.process.setuser(suid) + end + + if action.type == "view" then + tpl.render("view", { view = action.path }) + + elseif action.type == "call" then + local ok, mod = util.copcall(require, action.module) + if not ok then + error500(mod) + return + end + + local func = mod[action["function"]] + + assert(func ~= nil, + 'Cannot resolve function "' .. action["function"] .. '". Is it misspelled or local?') + + assert(type(func) == "function", + 'The symbol "' .. action["function"] .. '" does not refer to a function but data ' .. + 'of type "' .. type(func) .. '".') + + local argv = (type(action.parameters) == "table" and #action.parameters > 0) and { unpack(action.parameters) } or {} + for _, s in ipairs(requested_path_args) do + argv[#argv + 1] = s + end + + local ok, err = util.copcall(func, unpack(argv)) + if not ok then + error500(err) + end + + elseif action.type == "firstchild" then + local sub_request = find_subnode(page, requested_path_full, action.recurse) + if sub_request then + dispatch(sub_request) + else + tpl.render("empty_node_placeholder", getfenv(1)) + end + + elseif action.type == "alias" then + local sub_request = {} + for name in action.path:gmatch("[^/]+") do + sub_request[#sub_request + 1] = name + end + + for _, s in ipairs(requested_path_args) do + sub_request[#sub_request + 1] = s + end + + dispatch(sub_request) + + elseif action.type == "rewrite" then + local sub_request = { unpack(request) } + for i = 1, action.remove do + table.remove(sub_request, 1) + end + + local n = 1 + for s in action.path:gmatch("[^/]+") do + table.insert(sub_request, n, s) + n = n + 1 + end + + for _, s in ipairs(requested_path_args) do + sub_request[#sub_request + 1] = s + end + + dispatch(sub_request) + + elseif action.type == "template" then + tpl.render(action.path, getfenv(1)) + + elseif action.type == "cbi" then + _cbi({ config = action.config, model = action.path }, unpack(requested_path_args)) + + elseif action.type == "form" then + _form({ model = action.path }, unpack(requested_path_args)) + + else + local root = find_subnode(menu, {}, true) + if not root then + error404("No root node was registered, this usually happens if no module was installed.\n" .. + "Install luci-mod-admin-full and retry. " .. + "If the module is already installed, try removing the /tmp/luci-indexcache file.") + else + error404("No page is registered at '/" .. table.concat(requested_path_full, "/") .. "'.\n" .. + "If this url belongs to an extension, make sure it is properly installed.\n" .. + "If the extension was recently installed, try removing the /tmp/luci-indexcache file.") + end + end +end + +local function hash_filelist(files) + local fprint = {} + local n = 0 + + for i, file in ipairs(files) do + local st = fs.stat(file) + if st then + fprint[n + 1] = '%x' % st.ino + fprint[n + 2] = '%x' % st.mtime + fprint[n + 3] = '%x' % st.size + n = n + 3 + end + end + + return nixio.crypt(table.concat(fprint, "|"), "$1$"):sub(5):gsub("/", ".") +end + +local function read_cachefile(file, reader) + local euid = sys.process.info("uid") + local fuid = fs.stat(file, "uid") + local mode = fs.stat(file, "modestr") + + if euid ~= fuid or mode ~= "rw-------" then + return nil + end + + return reader(file) +end + +function createindex() + local controllers = { } + local base = "%s/controller/" % util.libpath() + local _, path + + for path in (fs.glob("%s*.lua" % base) or function() end) do + controllers[#controllers+1] = path + end + + for path in (fs.glob("%s*/*.lua" % base) or function() end) do + controllers[#controllers+1] = path + end + + local cachefile + + if indexcache then + cachefile = "%s.%s.lua" %{ indexcache, hash_filelist(controllers) } + + local res = read_cachefile(cachefile, function(path) return loadfile(path)() end) + if res then + index = res + return res + end + + for file in (fs.glob("%s.*.lua" % indexcache) or function() end) do + fs.unlink(file) + end + end + + index = {} + + for _, path in ipairs(controllers) do + local modname = "luci.controller." .. path:sub(#base+1, #path-4):gsub("/", ".") + local mod = require(modname) + assert(mod ~= true, + "Invalid controller file found\n" .. + "The file '" .. path .. "' contains an invalid module line.\n" .. + "Please verify whether the module name is set to '" .. modname .. + "' - It must correspond to the file path!") + + local idx = mod.index + if type(idx) == "function" then + index[modname] = idx + end + end + + if cachefile then + local f = nixio.open(cachefile, "w", 600) + f:writeall(util.get_bytecode(index)) + f:close() + end +end + +function createtree_json() + local json = require "luci.jsonc" + local tree = {} + + local schema = { + action = "table", + auth = "table", + cors = "boolean", + depends = "table", + order = "number", + setgroup = "string", + setuser = "string", + title = "string", + wildcard = "boolean" + } + + local files = {} + local cachefile + + for file in (fs.glob("/usr/share/luci/menu.d/*.json") or function() end) do + files[#files+1] = file + end + + if indexcache then + cachefile = "%s.%s.json" %{ indexcache, hash_filelist(files) } + + local res = read_cachefile(cachefile, function(path) return json.parse(fs.readfile(path) or "") end) + if res then + return res + end + + for file in (fs.glob("%s.*.json" % indexcache) or function() end) do + fs.unlink(file) + end + end + + for _, file in ipairs(files) do + local data = json.parse(fs.readfile(file) or "") + if type(data) == "table" then + for path, spec in pairs(data) do + if type(spec) == "table" then + local node = tree + + for s in path:gmatch("[^/]+") do + if s == "*" then + node.wildcard = true + break + end + + node.children = node.children or {} + node.children[s] = node.children[s] or {} + node = node.children[s] + end + + if node ~= tree then + for k, t in pairs(schema) do + if type(spec[k]) == t then + node[k] = spec[k] + end + end + + node.satisfied = check_depends(spec) + end + end + end + end + end + + if cachefile then + local f = nixio.open(cachefile, "w", 600) + f:writeall(json.stringify(tree)) + f:close() + end + + return tree +end + +-- Build the index before if it does not exist yet. +function createtree() + if not index then + createindex() + end + + local ctx = context + local tree = {nodes={}, inreq=true} + + ctx.treecache = setmetatable({}, {__mode="v"}) + ctx.tree = tree + + local scope = setmetatable({}, {__index = luci.dispatcher}) + + for k, v in pairs(index) do + scope._NAME = k + setfenv(v, scope) + v() + end + + return tree +end + +function assign(path, clone, title, order) + local obj = node(unpack(path)) + obj.nodes = nil + obj.module = nil + + obj.title = title + obj.order = order + + setmetatable(obj, {__index = _create_node(clone)}) + + return obj +end + +function entry(path, target, title, order) + local c = node(unpack(path)) + + c.target = target + c.title = title + c.order = order + c.module = getfenv(2)._NAME + + return c +end + +-- enabling the node. +function get(...) + return _create_node({...}) +end + +function node(...) + local c = _create_node({...}) + + c.module = getfenv(2)._NAME + c.auto = nil + + return c +end + +function lookup(...) + local i, path = nil, {} + for i = 1, select('#', ...) do + local name, arg = nil, tostring(select(i, ...)) + for name in arg:gmatch("[^/]+") do + path[#path+1] = name + end + end + + for i = #path, 1, -1 do + local node = context.treecache[table.concat(path, ".", 1, i)] + if node and (i == #path or node.leaf) then + return node, build_url(unpack(path)) + end + end +end + +function _create_node(path) + if #path == 0 then + return context.tree + end + + local name = table.concat(path, ".") + local c = context.treecache[name] + + if not c then + local last = table.remove(path) + local parent = _create_node(path) + + c = {nodes={}, auto=true, inreq=true} + + parent.nodes[last] = c + context.treecache[name] = c + end + + return c +end + +-- Subdispatchers -- + +function firstchild() + return { type = "firstchild" } +end + +function firstnode() + return { type = "firstnode" } +end + +function alias(...) + return { type = "alias", req = { ... } } +end + +function rewrite(n, ...) + return { type = "rewrite", n = n, req = { ... } } +end + +function call(name, ...) + return { type = "call", argv = {...}, name = name } +end + +function post_on(params, name, ...) + return { + type = "call", + post = params, + argv = { ... }, + name = name + } +end + +function post(...) + return post_on(true, ...) +end + + +function template(name) + return { type = "template", view = name } +end + +function view(name) + return { type = "view", view = name } +end + + +function _cbi(self, ...) + local cbi = require "luci.cbi" + local tpl = require "luci.template" + local http = require "luci.http" + local util = require "luci.util" + + local config = self.config or {} + local maps = cbi.load(self.model, ...) + + local state = nil + + local function has_uci_access(config, level) + local rv = util.ubus("session", "access", { + ubus_rpc_session = context.authsession, + scope = "uci", object = config, + ["function"] = level + }) + + return (type(rv) == "table" and rv.access == true) or false + end + + local i, res + for i, res in ipairs(maps) do + if util.instanceof(res, cbi.SimpleForm) then + io.stderr:write("Model %s returns SimpleForm but is dispatched via cbi(),\n" + % self.model) + + io.stderr:write("please change %s to use the form() action instead.\n" + % table.concat(context.request, "/")) + end + + res.flow = config + local cstate = res:parse() + if cstate and (not state or cstate < state) then + state = cstate + end + end + + local function _resolve_path(path) + return type(path) == "table" and build_url(unpack(path)) or path + end + + if config.on_valid_to and state and state > 0 and state < 2 then + http.redirect(_resolve_path(config.on_valid_to)) + return + end + + if config.on_changed_to and state and state > 1 then + http.redirect(_resolve_path(config.on_changed_to)) + return + end + + if config.on_success_to and state and state > 0 then + http.redirect(_resolve_path(config.on_success_to)) + return + end + + if config.state_handler then + if not config.state_handler(state, maps) then + return + end + end + + http.header("X-CBI-State", state or 0) + + if not config.noheader then + tpl.render("cbi/header", {state = state}) + end + + local redirect + local messages + local applymap = false + local pageaction = true + local parsechain = { } + local writable = false + + for i, res in ipairs(maps) do + if res.apply_needed and res.parsechain then + local c + for _, c in ipairs(res.parsechain) do + parsechain[#parsechain+1] = c + end + applymap = true + end + + if res.redirect then + redirect = redirect or res.redirect + end + + if res.pageaction == false then + pageaction = false + end + + if res.message then + messages = messages or { } + messages[#messages+1] = res.message + end + end + + for i, res in ipairs(maps) do + local is_readable_map = has_uci_access(res.config, "read") + local is_writable_map = has_uci_access(res.config, "write") + + writable = writable or is_writable_map + + res:render({ + firstmap = (i == 1), + redirect = redirect, + messages = messages, + pageaction = pageaction, + parsechain = parsechain, + readable = is_readable_map, + writable = is_writable_map + }) + end + + if not config.nofooter then + tpl.render("cbi/footer", { + flow = config, + pageaction = pageaction, + redirect = redirect, + state = state, + autoapply = config.autoapply, + trigger_apply = applymap, + writable = writable + }) + end +end + +function cbi(model, config) + return { + type = "cbi", + post = { ["cbi.submit"] = true }, + config = config, + model = model + } +end + + +function arcombine(trg1, trg2) + return { + type = "arcombine", + env = getfenv(), + targets = {trg1, trg2} + } +end + + +function _form(self, ...) + local cbi = require "luci.cbi" + local tpl = require "luci.template" + local http = require "luci.http" + + local maps = luci.cbi.load(self.model, ...) + local state = nil + + local i, res + for i, res in ipairs(maps) do + local cstate = res:parse() + if cstate and (not state or cstate < state) then + state = cstate + end + end + + http.header("X-CBI-State", state or 0) + tpl.render("header") + for i, res in ipairs(maps) do + res:render() + end + tpl.render("footer") +end + +function form(model) + return { + type = "form", + post = { ["cbi.submit"] = true }, + model = model + } +end + +translate = i18n.translate + +-- This function does not actually translate the given argument but +-- is used by build/i18n-scan.pl to find translatable entries. +function _(text) + return text +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/dispatcher.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/dispatcher.luac new file mode 100644 index 000000000000..caad21ada033 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/dispatcher.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/http.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/http.lua new file mode 100644 index 000000000000..20b55f2854ff --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/http.lua @@ -0,0 +1,554 @@ +-- Copyright 2008 Steven Barth +-- Copyright 2010-2018 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. + +local util = require "luci.util" +local coroutine = require "coroutine" +local table = require "table" +local lhttp = require "lucihttp" +local nixio = require "nixio" +local ltn12 = require "luci.ltn12" + +local table, ipairs, pairs, type, tostring, tonumber, error = + table, ipairs, pairs, type, tostring, tonumber, error + +module "luci.http" + +HTTP_MAX_CONTENT = 1024*100 -- 100 kB maximum content size + +context = util.threadlocal() + +Request = util.class() +function Request.__init__(self, env, sourcein, sinkerr) + self.input = sourcein + self.error = sinkerr + + + -- File handler nil by default to let .content() work + self.filehandler = nil + + -- HTTP-Message table + self.message = { + env = env, + headers = {}, + params = urldecode_params(env.QUERY_STRING or ""), + } + + self.parsed_input = false +end + +function Request.formvalue(self, name, noparse) + if not noparse and not self.parsed_input then + self:_parse_input() + end + + if name then + return self.message.params[name] + else + return self.message.params + end +end + +function Request.formvaluetable(self, prefix) + local vals = {} + prefix = prefix and prefix .. "." or "." + + if not self.parsed_input then + self:_parse_input() + end + + local void = self.message.params[nil] + for k, v in pairs(self.message.params) do + if k:find(prefix, 1, true) == 1 then + vals[k:sub(#prefix + 1)] = tostring(v) + end + end + + return vals +end + +function Request.content(self) + if not self.parsed_input then + self:_parse_input() + end + + return self.message.content, self.message.content_length +end + +function Request.getcookie(self, name) + return lhttp.header_attribute("cookie; " .. (self:getenv("HTTP_COOKIE") or ""), name) +end + +function Request.getenv(self, name) + if name then + return self.message.env[name] + else + return self.message.env + end +end + +function Request.setfilehandler(self, callback) + self.filehandler = callback + + if not self.parsed_input then + return + end + + -- If input has already been parsed then uploads are stored as unlinked + -- temporary files pointed to by open file handles in the parameter + -- value table. Loop all params, and invoke the file callback for any + -- param with an open file handle. + local name, value + for name, value in pairs(self.message.params) do + if type(value) == "table" then + while value.fd do + local data = value.fd:read(1024) + local eof = (not data or data == "") + + callback(value, data, eof) + + if eof then + value.fd:close() + value.fd = nil + end + end + end + end +end + +function Request._parse_input(self) + parse_message_body( + self.input, + self.message, + self.filehandler + ) + self.parsed_input = true +end + +function close() + if not context.eoh then + context.eoh = true + coroutine.yield(3) + end + + if not context.closed then + context.closed = true + coroutine.yield(5) + end +end + +function content() + return context.request:content() +end + +function formvalue(name, noparse) + return context.request:formvalue(name, noparse) +end + +function formvaluetable(prefix) + return context.request:formvaluetable(prefix) +end + +function getcookie(name) + return context.request:getcookie(name) +end + +-- or the environment table itself. +function getenv(name) + return context.request:getenv(name) +end + +function setfilehandler(callback) + return context.request:setfilehandler(callback) +end + +function header(key, value) + if not context.headers then + context.headers = {} + end + context.headers[key:lower()] = value + coroutine.yield(2, key, value) +end + +function prepare_content(mime) + if not context.headers or not context.headers["content-type"] then + if mime == "application/xhtml+xml" then + if not getenv("HTTP_ACCEPT") or + not getenv("HTTP_ACCEPT"):find("application/xhtml+xml", nil, true) then + mime = "text/html; charset=UTF-8" + end + header("Vary", "Accept") + end + header("Content-Type", mime) + end +end + +function source() + return context.request.input +end + +function status(code, message) + code = code or 200 + message = message or "OK" + context.status = code + coroutine.yield(1, code, message) +end + +-- This function is as a valid LTN12 sink. +-- If the content chunk is nil this function will automatically invoke close. +function write(content, src_err) + if not content then + if src_err then + error(src_err) + else + close() + end + return true + elseif #content == 0 then + return true + else + if not context.eoh then + if not context.status then + status() + end + if not context.headers or not context.headers["content-type"] then + header("Content-Type", "text/html; charset=utf-8") + end + if not context.headers["cache-control"] then + header("Cache-Control", "no-cache") + header("Expires", "0") + end + if not context.headers["x-frame-options"] then + header("X-Frame-Options", "SAMEORIGIN") + end + if not context.headers["x-xss-protection"] then + header("X-XSS-Protection", "1; mode=block") + end + if not context.headers["x-content-type-options"] then + header("X-Content-Type-Options", "nosniff") + end + + context.eoh = true + coroutine.yield(3) + end + coroutine.yield(4, content) + return true + end +end + +function splice(fd, size) + coroutine.yield(6, fd, size) +end + +function redirect(url) + if url == "" then url = "/" end + status(302, "Found") + header("Location", url) + close() +end + +function build_querystring(q) + local s, n, k, v = {}, 1, nil, nil + + for k, v in pairs(q) do + s[n+0] = (n == 1) and "?" or "&" + s[n+1] = util.urlencode(k) + s[n+2] = "=" + s[n+3] = util.urlencode(v) + n = n + 4 + end + + return table.concat(s, "") +end + +urldecode = util.urldecode + +urlencode = util.urlencode + +function write_json(x) + util.serialize_json(x, write) +end + +-- from given url or string. Returns a table with urldecoded values. +-- Simple parameters are stored as string values associated with the parameter +-- name within the table. Parameters with multiple values are stored as array +-- containing the corresponding values. +function urldecode_params(url, tbl) + local parser, name + local params = tbl or { } + + parser = lhttp.urlencoded_parser(function (what, buffer, length) + if what == parser.TUPLE then + name, value = nil, nil + elseif what == parser.NAME then + name = lhttp.urldecode(buffer) + elseif what == parser.VALUE and name then + params[name] = lhttp.urldecode(buffer) or "" + end + + return true + end) + + if parser then + parser:parse((url or ""):match("[^?]*$")) + parser:parse(nil) + end + + return params +end + +-- separated by "&". Tables are encoded as parameters with multiple values by +-- repeating the parameter name with each value. +function urlencode_params(tbl) + local k, v + local n, enc = 1, {} + for k, v in pairs(tbl) do + if type(v) == "table" then + local i, v2 + for i, v2 in ipairs(v) do + if enc[1] then + enc[n] = "&" + n = n + 1 + end + + enc[n+0] = lhttp.urlencode(k) + enc[n+1] = "=" + enc[n+2] = lhttp.urlencode(v2) + n = n + 3 + end + else + if enc[1] then + enc[n] = "&" + n = n + 1 + end + + enc[n+0] = lhttp.urlencode(k) + enc[n+1] = "=" + enc[n+2] = lhttp.urlencode(v) + n = n + 3 + end + end + + return table.concat(enc, "") +end + +-- Content-Type. Stores all extracted data associated with its parameter name +-- in the params table within the given message object. Multiple parameter +-- values are stored as tables, ordinary ones as strings. +-- If an optional file callback function is given then it is fed with the +-- file contents chunk by chunk and only the extracted file name is stored +-- within the params table. The callback function will be called subsequently +-- with three arguments: +-- o Table containing decoded (name, file) and raw (headers) mime header data +-- o String value containing a chunk of the file data +-- o Boolean which indicates whether the current chunk is the last one (eof) +function mimedecode_message_body(src, msg, file_cb) + local parser, header, field + local len, maxlen = 0, tonumber(msg.env.CONTENT_LENGTH or nil) + + parser, err = lhttp.multipart_parser(msg.env.CONTENT_TYPE, function (what, buffer, length) + if what == parser.PART_INIT then + field = { } + + elseif what == parser.HEADER_NAME then + header = buffer:lower() + + elseif what == parser.HEADER_VALUE and header then + if header:lower() == "content-disposition" and + lhttp.header_attribute(buffer, nil) == "form-data" + then + field.name = lhttp.header_attribute(buffer, "name") + field.file = lhttp.header_attribute(buffer, "filename") + field[1] = field.file + end + + if field.headers then + field.headers[header] = buffer + else + field.headers = { [header] = buffer } + end + + elseif what == parser.PART_BEGIN then + return not field.file + + elseif what == parser.PART_DATA and field.name and length > 0 then + if field.file then + if file_cb then + file_cb(field, buffer, false) + msg.params[field.name] = msg.params[field.name] or field + else + if not field.fd then + field.fd = nixio.mkstemp(field.name) + end + + if field.fd then + field.fd:write(buffer) + msg.params[field.name] = msg.params[field.name] or field + end + end + else + field.value = buffer + end + + elseif what == parser.PART_END and field.name then + if field.file and msg.params[field.name] then + if file_cb then + file_cb(field, "", true) + elseif field.fd then + field.fd:seek(0, "set") + end + else + local val = msg.params[field.name] + + if type(val) == "table" then + val[#val+1] = field.value or "" + elseif val ~= nil then + msg.params[field.name] = { val, field.value or "" } + else + msg.params[field.name] = field.value or "" + end + end + + field = nil + + elseif what == parser.ERROR then + err = buffer + end + + return true + end, HTTP_MAX_CONTENT) + + return ltn12.pump.all(src, function (chunk) + len = len + (chunk and #chunk or 0) + + if maxlen and len > maxlen + 2 then + return nil, "Message body size exceeds Content-Length" + end + + if not parser or not parser:parse(chunk) then + return nil, err + end + + return true + end) +end + +-- Content-Type. Stores all extracted data associated with its parameter name +-- in the params table within the given message object. Multiple parameter +-- values are stored as tables, ordinary ones as strings. +function urldecode_message_body(src, msg) + local err, name, value, parser + local len, maxlen = 0, tonumber(msg.env.CONTENT_LENGTH or nil) + + parser = lhttp.urlencoded_parser(function (what, buffer, length) + if what == parser.TUPLE then + name, value = nil, nil + elseif what == parser.NAME then + name = lhttp.urldecode(buffer, lhttp.DECODE_PLUS) + elseif what == parser.VALUE and name then + local val = msg.params[name] + + if type(val) == "table" then + val[#val+1] = lhttp.urldecode(buffer, lhttp.DECODE_PLUS) or "" + elseif val ~= nil then + msg.params[name] = { val, lhttp.urldecode(buffer, lhttp.DECODE_PLUS) or "" } + else + msg.params[name] = lhttp.urldecode(buffer, lhttp.DECODE_PLUS) or "" + end + elseif what == parser.ERROR then + err = buffer + end + + return true + end, HTTP_MAX_CONTENT) + + return ltn12.pump.all(src, function (chunk) + len = len + (chunk and #chunk or 0) + + if maxlen and len > maxlen + 2 then + return nil, "Message body size exceeds Content-Length" + elseif len > HTTP_MAX_CONTENT then + return nil, "Message body size exceeds maximum allowed length" + end + + if not parser or not parser:parse(chunk) then + return nil, err + end + + return true + end) +end + +-- This function will examine the Content-Type within the given message object +-- to select the appropriate content decoder. +-- Currently the application/x-www-urlencoded and application/form-data +-- mime types are supported. If the encountered content encoding can't be +-- handled then the whole message body will be stored unaltered as "content" +-- property within the given message object. +function parse_message_body(src, msg, filecb) + if msg.env.CONTENT_LENGTH or msg.env.REQUEST_METHOD == "POST" then + local ctype = lhttp.header_attribute(msg.env.CONTENT_TYPE, nil) + + -- Is it multipart/mime ? + if ctype == "multipart/form-data" then + return mimedecode_message_body(src, msg, filecb) + + -- Is it application/x-www-form-urlencoded ? + elseif ctype == "application/x-www-form-urlencoded" then + return urldecode_message_body(src, msg) + + end + + -- Unhandled encoding + -- If a file callback is given then feed it chunk by chunk, else + -- store whole buffer in message.content + local sink + + -- If we have a file callback then feed it + if type(filecb) == "function" then + local meta = { + name = "raw", + encoding = msg.env.CONTENT_TYPE + } + sink = function( chunk ) + if chunk then + return filecb(meta, chunk, false) + else + return filecb(meta, nil, true) + end + end + -- ... else append to .content + else + msg.content = "" + msg.content_length = 0 + + sink = function( chunk ) + if chunk then + if ( msg.content_length + #chunk ) <= HTTP_MAX_CONTENT then + msg.content = msg.content .. chunk + msg.content_length = msg.content_length + #chunk + return true + else + return nil, "POST data exceeds maximum allowed length" + end + end + return true + end + end + + -- Pump data... + while true do + local ok, err = ltn12.pump.step( src, sink ) + + if not ok and err then + return nil, err + elseif not ok then -- eof + return true + end + end + + return true + end + + return false +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/http.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/http.luac new file mode 100644 index 000000000000..fb8332775b74 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/http.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/i18n.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/i18n.lua new file mode 100644 index 000000000000..323912b65022 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/i18n.lua @@ -0,0 +1,55 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +local tparser = require "luci.template.parser" +local util = require "luci.util" +local tostring = tostring + +module "luci.i18n" + +i18ndir = util.libpath() .. "/i18n/" +context = util.threadlocal() +default = "en" + + +function setlanguage(lang) + local code, subcode = lang:match("^([A-Za-z][A-Za-z])[%-_]([A-Za-z][A-Za-z])$") + if not (code and subcode) then + subcode = lang:match("^([A-Za-z][A-Za-z])$") + if not subcode then + return nil + end + end + + context.parent = code and code:lower() + context.lang = context.parent and context.parent.."-"..subcode:lower() or subcode:lower() + + if tparser.load_catalog(context.lang, i18ndir) and + tparser.change_catalog(context.lang) + then + return context.lang + + elseif context.parent then + if tparser.load_catalog(context.parent, i18ndir) and + tparser.change_catalog(context.parent) + then + return context.parent + end + end + + return nil +end + +function translate(key) + return tparser.translate(key) or key +end + +function translatef(key, ...) + return tostring(translate(key)):format(...) +end + +function dump() + local rv = {} + tparser.get_translations(function(k, v) rv[k] = v end) + return rv +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/i18n.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/i18n.luac new file mode 100644 index 000000000000..a3a32578b3b4 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/i18n.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/ltn12.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/ltn12.lua new file mode 100644 index 000000000000..3a7268ccaef6 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/ltn12.lua @@ -0,0 +1,316 @@ +--[[ +LuaSocket 2.0.2 license +Copyright � 2004-2007 Diego Nehab + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. +]]-- +--[[ + Changes made by LuCI project: + * Renamed to luci.ltn12 to avoid collisions with luasocket + * Added inline documentation +]]-- +----------------------------------------------------------------------------- +-- LTN12 - Filters, sources, sinks and pumps. +-- LuaSocket toolkit. +-- Author: Diego Nehab +-- RCS ID: $Id$ +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module +----------------------------------------------------------------------------- +local string = require("string") +local table = require("table") +local base = _G + +-- See http://lua-users.org/wiki/FiltersSourcesAndSinks for design concepts +module("luci.ltn12") + +filter = {} +source = {} +sink = {} +pump = {} + +-- 2048 seems to be better in windows... +BLOCKSIZE = 2048 +_VERSION = "LTN12 1.0.1" + +----------------------------------------------------------------------------- +-- Filter stuff +----------------------------------------------------------------------------- + + +-- by passing it each chunk and updating a context between calls. +function filter.cycle(low, ctx, extra) + base.assert(low) + return function(chunk) + local ret + ret, ctx = low(ctx, chunk, extra) + return ret + end +end + +-- (thanks to Wim Couwenberg) +function filter.chain(...) + local n = table.getn(arg) + local top, index = 1, 1 + local retry = "" + return function(chunk) + retry = chunk and retry + while true do + if index == top then + chunk = arg[index](chunk) + if chunk == "" or top == n then return chunk + elseif chunk then index = index + 1 + else + top = top+1 + index = top + end + else + chunk = arg[index](chunk or "") + if chunk == "" then + index = index - 1 + chunk = retry + elseif chunk then + if index == n then return chunk + else index = index + 1 end + else base.error("filter returned inappropriate nil") end + end + end + end +end + +----------------------------------------------------------------------------- +-- Source stuff +----------------------------------------------------------------------------- + + +-- create an empty source +local function empty() + return nil +end + +function source.empty() + return empty +end + +function source.error(err) + return function() + return nil, err + end +end + +function source.file(handle, io_err) + if handle then + return function() + local chunk = handle:read(BLOCKSIZE) + if chunk and chunk:len() == 0 then chunk = nil end + if not chunk then handle:close() end + return chunk + end + else return source.error(io_err or "unable to open file") end +end + +function source.simplify(src) + base.assert(src) + return function() + local chunk, err_or_new = src() + src = err_or_new or src + if not chunk then return nil, err_or_new + else return chunk end + end +end + +function source.string(s) + if s then + local i = 1 + return function() + local chunk = string.sub(s, i, i+BLOCKSIZE-1) + i = i + BLOCKSIZE + if chunk ~= "" then return chunk + else return nil end + end + else return source.empty() end +end + +function source.rewind(src) + base.assert(src) + local t = {} + return function(chunk) + if not chunk then + chunk = table.remove(t) + if not chunk then return src() + else return chunk end + else + t[#t+1] = chunk + end + end +end + +function source.chain(src, f) + base.assert(src and f) + local last_in, last_out = "", "" + local state = "feeding" + local err + return function() + if not last_out then + base.error('source is empty!', 2) + end + while true do + if state == "feeding" then + last_in, err = src() + if err then return nil, err end + last_out = f(last_in) + if not last_out then + if last_in then + base.error('filter returned inappropriate nil') + else + return nil + end + elseif last_out ~= "" then + state = "eating" + if last_in then last_in = "" end + return last_out + end + else + last_out = f(last_in) + if last_out == "" then + if last_in == "" then + state = "feeding" + else + base.error('filter returned ""') + end + elseif not last_out then + if last_in then + base.error('filter returned inappropriate nil') + else + return nil + end + else + return last_out + end + end + end + end +end + +-- Sources will be used one after the other, as if they were concatenated +-- (thanks to Wim Couwenberg) +function source.cat(...) + local src = table.remove(arg, 1) + return function() + while src do + local chunk, err = src() + if chunk then return chunk end + if err then return nil, err end + src = table.remove(arg, 1) + end + end +end + +----------------------------------------------------------------------------- +-- Sink stuff +----------------------------------------------------------------------------- + + +function sink.table(t) + t = t or {} + local f = function(chunk, err) + if chunk then t[#t+1] = chunk end + return 1 + end + return f, t +end + +function sink.simplify(snk) + base.assert(snk) + return function(chunk, err) + local ret, err_or_new = snk(chunk, err) + if not ret then return nil, err_or_new end + snk = err_or_new or snk + return 1 + end +end + +function sink.file(handle, io_err) + if handle then + return function(chunk, err) + if not chunk then + handle:close() + return 1 + else return handle:write(chunk) end + end + else return sink.error(io_err or "unable to open file") end +end + +-- creates a sink that discards data +local function null() + return 1 +end + +function sink.null() + return null +end + +function sink.error(err) + return function() + return nil, err + end +end + +function sink.chain(f, snk) + base.assert(f and snk) + return function(chunk, err) + if chunk ~= "" then + local filtered = f(chunk) + local done = chunk and "" + while true do + local ret, snkerr = snk(filtered, err) + if not ret then return nil, snkerr end + if filtered == done then return 1 end + filtered = f(done) + end + else return 1 end + end +end + +----------------------------------------------------------------------------- +-- Pump stuff +----------------------------------------------------------------------------- + + +function pump.step(src, snk) + local chunk, src_err = src() + local ret, snk_err = snk(chunk, src_err) + if chunk and ret then return 1 + else return nil, src_err or snk_err end +end + +function pump.all(src, snk, step) + base.assert(src and snk) + step = step or pump.step + while true do + local ret, err = step(src, snk) + if not ret then + if err then return nil, err + else return 1 end + end + end +end + diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/ltn12.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/ltn12.luac new file mode 100644 index 000000000000..cb138de46d31 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/ltn12.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/cbi/hwnat.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/cbi/hwnat.lua new file mode 100755 index 000000000000..a23236e1161e --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/cbi/hwnat.lua @@ -0,0 +1,12 @@ + +m = Map("hwnat", translate("Hardware NAT Acceleration"), + translate("The Hardware NAT Acceleration designed for reducing cpu loading")) + +s = m:section(TypedSection, "hwnat", "HWNAT") +s.addremove=false +s.anonymous = true +enable = s:option(Flag,"enabled",translate("Enable")) + +m:section(SimpleSection).template = "admin_mtk/hwnat_status" + +return m \ No newline at end of file diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/cbi/hwnat.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/cbi/hwnat.luac new file mode 100644 index 000000000000..6a9e630666ed Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/cbi/hwnat.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/cbi/ipsec.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/cbi/ipsec.lua new file mode 100755 index 000000000000..9008c74f3b7d --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/cbi/ipsec.lua @@ -0,0 +1,122 @@ +require "luci.model.uci" +require "luci.sys" +local sec_name +local cursor = luci.model.uci.cursor() +cursor:foreach("ipsec", "remote", function(s) sec_name = s['.name'] end) +m = Map("ipsec", translate("IP Security")) + +s = m:section(TypedSection, "remote") +s.anonymous = true +o=s:option(Value, "localGatewayName", translate("Local Gateway Name")) +o.default = sec_name + +o=s:option(ListValue, "enabled", translate("IPSec VPN")) +o.widget="radio" +o.orientation = "horizontal" +o:value("0", "Disable") -- Key and value pairs +o:value("1", "Enable") + +tunnel = m:section(NamedSection, "TUNNEL") +o=tunnel:option(Value, "local_subnet", translate("Local Group Subnet")) +o.rmempty= false +o.datatype = 'ip4addr' + +s = m:section(TypedSection, "remote") +s.anonymous = true +o=s:option(Value, "gateway", translate("Remote Gateway IP Address")) +o.rmempty= false +o.datatype = 'ip4addr' + +tunnel = m:section(NamedSection, "TUNNEL") +o=tunnel:option(Value, "remote_subnet", translate("Remote Group Subnet")) +o.rmempty= false +o.datatype = 'ip4addr' + +s = m:section(TypedSection, "remote") +s.anonymous = true +p = s:option(ListValue, "authentication_method",translate"Keying Mode") +p:value("psk", "PSK") + +o=s:option(Value, "pre_shared_key", translate("Pre-shared Key")) +o.rmempty= false + +tunnel = m:section(NamedSection, "TUNNEL") +x = tunnel:option(ListValue, "mode",translate"Auto Mode") +x:value("add", "Add") +x:value("route", "Route") +x:value("start", "Start") +x:value("ignore", "Ignore") + + +d = tunnel:option(ListValue, "keyexchange",translate"Key Exchange") +d:value("ikev2", "ikev2") +d.default = "ikev2" + +c = tunnel:option(Value, "ikelifetime",translate"IKE Lifetime") +c.default = "10800" + +e = tunnel:option(Value, "lifetime",translate"Key Lifetime") +e.default = "3600" + +z = m:section(NamedSection, "phase_1_settings", "crypto_proposal", "Phase 1 Settings") +z.addremove = false + +q = z:option(ListValue, "encryption_algorithm",translate"Encryption") +q:value("aes128", "AES128") +q:value("aes192", "AES192") +q:value("aes256", "AES256") +q.default = "aes128" + +w = z:option(ListValue, "hash_algorithm",translate"Authentication") +w:value("sha1", "SHA1") +w:value("sha256", "SHA256") +w.default = "sha1" + +r = z:option(ListValue, "dh_group",translate"Group") +r:value("modp768", "modp768") +r:value("modp1024", "modp1024") +r:value("modp1536", "modp1536") +r.default = "modp768" + +j = m:section(NamedSection, "phase_2_settings", "crypto_proposal", "Phase 2 Settings") +l = j:option(ListValue, "encryption_algorithm",translate"Encryption") +l:value("aes128", "AES128") +l:value("aes192", "AES192") +l:value("aes256", "AES256") +l.default = "aes128" + +t = j:option(ListValue, "hash_algorithm",translate"Authentication") +t:value("sha1", "SHA1") +t:value("sha256", "SHA256") +t.default = "sha1" + +u = j:option(ListValue, "dh_group",translate"Group") +u:value("modp768", "modp768") +u:value("modp1024", "modp1024") +u:value("modp1536", "modp1536") +u.default = "modp768" + +con = m:section(TypedSection, "remote") +con.anonymous = true +status = con:option(Value, "vpn_status", translate"Status") +status.default = "Disconnected/Command not found" + +function m.on_commit(Map) + + local cur = luci.model.uci.cursor() + CBI_PREFIX = "cbid.ipsec." + local org_sec_name,new_sec_name + cur:foreach("ipsec", "remote", function(s) org_sec_name = s['.name'] end) + + local new_sec_name = luci.http.formvalue(CBI_PREFIX ..org_sec_name.. ".localGatewayName") + + if(new_sec_name ~= org_sec_name) then + --cur:rename('ipsec',org_sec_name,new_sec_name) + cur:save('ipsec') + cur:commit('ipsec') + end +end + +m:section(SimpleSection).template = "admin_mtk/mtk_ipsec_view" + +return m diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/cbi/ipsec.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/cbi/ipsec.luac new file mode 100644 index 000000000000..4c92779e63f5 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/cbi/ipsec.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/uci.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/uci.lua new file mode 100644 index 000000000000..816f6f20538a --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/uci.lua @@ -0,0 +1,508 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +local os = require "os" +local util = require "luci.util" +local table = require "table" + + +local setmetatable, rawget, rawset = setmetatable, rawget, rawset +local require, getmetatable, assert = require, getmetatable, assert +local error, pairs, ipairs, select = error, pairs, ipairs, select +local type, tostring, tonumber, unpack = type, tostring, tonumber, unpack + +-- The typical workflow for UCI is: Get a cursor instance from the +-- cursor factory, modify data (via Cursor.add, Cursor.delete, etc.), +-- save the changes to the staging area via Cursor.save and finally +-- Cursor.commit the data to the actual config files. +-- LuCI then needs to Cursor.apply the changes so daemons etc. are +-- reloaded. +module "luci.model.uci" + +local ERRSTR = { + "Invalid command", + "Invalid argument", + "Method not found", + "Entry not found", + "No data", + "Permission denied", + "Timeout", + "Not supported", + "Unknown error", + "Connection failed" +} + +local session_id = nil + +local function call(cmd, args) + if type(args) == "table" and session_id then + args.ubus_rpc_session = session_id + end + return util.ubus("uci", cmd, args) +end + + +function cursor() + return _M +end + +function cursor_state() + return _M +end + +function substate(self) + return self +end + + +function get_confdir(self) + return "/etc/config" +end + +function get_savedir(self) + return "/tmp/.uci" +end + +function get_session_id(self) + return session_id +end + +function set_confdir(self, directory) + return false +end + +function set_savedir(self, directory) + return false +end + +function set_session_id(self, id) + session_id = id + return true +end + + +function load(self, config) + return true +end + +function save(self, config) + return true +end + +function unload(self, config) + return true +end + + +function changes(self, config) + local rv, err = call("changes", { config = config }) + + if type(rv) == "table" and type(rv.changes) == "table" then + return rv.changes + elseif err then + return nil, ERRSTR[err] + else + return { } + end +end + + +function revert(self, config) + local _, err = call("revert", { config = config }) + return (err == nil), ERRSTR[err] +end + +function commit(self, config) + local _, err = call("commit", { config = config }) + return (err == nil), ERRSTR[err] +end + +function apply(self, rollback) + local _, err + + if rollback then + local sys = require "luci.sys" + local conf = require "luci.config" + local timeout = tonumber(conf and conf.apply and conf.apply.rollback or 90) or 0 + + _, err = call("apply", { + timeout = (timeout > 90) and timeout or 90, + rollback = true + }) + + if not err then + local now = os.time() + local token = sys.uniqueid(16) + + util.ubus("session", "set", { + ubus_rpc_session = "00000000000000000000000000000000", + values = { + rollback = { + token = token, + session = session_id, + timeout = now + timeout + } + } + }) + + return token + end + else + _, err = call("changes", {}) + + if not err then + if type(_) == "table" and type(_.changes) == "table" then + local k, v + for k, v in pairs(_.changes) do + _, err = call("commit", { config = k }) + if err then + break + end + end + end + end + + if not err then + _, err = call("apply", { rollback = false }) + end + end + + return (err == nil), ERRSTR[err] +end + +function confirm(self, token) + local is_pending, time_remaining, rollback_sid, rollback_token = self:rollback_pending() + + if is_pending then + if token ~= rollback_token then + return false, "Permission denied" + end + + local _, err = util.ubus("uci", "confirm", { + ubus_rpc_session = rollback_sid + }) + + if not err then + util.ubus("session", "set", { + ubus_rpc_session = "00000000000000000000000000000000", + values = { rollback = {} } + }) + end + + return (err == nil), ERRSTR[err] + end + + return false, "No data" +end + +function rollback(self) + local is_pending, time_remaining, rollback_sid = self:rollback_pending() + + if is_pending then + local _, err = util.ubus("uci", "rollback", { + ubus_rpc_session = rollback_sid + }) + + if not err then + util.ubus("session", "set", { + ubus_rpc_session = "00000000000000000000000000000000", + values = { rollback = {} } + }) + end + + return (err == nil), ERRSTR[err] + end + + return false, "No data" +end + +function rollback_pending(self) + local rv, err = util.ubus("session", "get", { + ubus_rpc_session = "00000000000000000000000000000000", + keys = { "rollback" } + }) + + local now = os.time() + + if type(rv) == "table" and + type(rv.values) == "table" and + type(rv.values.rollback) == "table" and + type(rv.values.rollback.token) == "string" and + type(rv.values.rollback.session) == "string" and + type(rv.values.rollback.timeout) == "number" and + rv.values.rollback.timeout > now + then + return true, + rv.values.rollback.timeout - now, + rv.values.rollback.session, + rv.values.rollback.token + end + + return false, ERRSTR[err] +end + + +function foreach(self, config, stype, callback) + if type(callback) == "function" then + local rv, err = call("get", { + config = config, + type = stype + }) + + if type(rv) == "table" and type(rv.values) == "table" then + local sections = { } + local res = false + local index = 1 + + local _, section + for _, section in pairs(rv.values) do + section[".index"] = section[".index"] or index + sections[index] = section + index = index + 1 + end + + table.sort(sections, function(a, b) + return a[".index"] < b[".index"] + end) + + for _, section in ipairs(sections) do + local continue = callback(section) + res = true + if continue == false then + break + end + end + return res + else + return false, ERRSTR[err] or "No data" + end + else + return false, "Invalid argument" + end +end + +local function _get(self, operation, config, section, option) + if section == nil then + return nil + elseif type(option) == "string" and option:byte(1) ~= 46 then + local rv, err = call(operation, { + config = config, + section = section, + option = option + }) + + if type(rv) == "table" then + return rv.value or nil + elseif err then + return false, ERRSTR[err] + else + return nil + end + elseif option == nil then + local values = self:get_all(config, section) + if values then + return values[".type"], values[".name"] + else + return nil + end + else + return false, "Invalid argument" + end +end + +function get(self, ...) + return _get(self, "get", ...) +end + +function get_state(self, ...) + return _get(self, "state", ...) +end + +function get_all(self, config, section) + local rv, err = call("get", { + config = config, + section = section + }) + + if type(rv) == "table" and type(rv.values) == "table" then + return rv.values + elseif err then + return false, ERRSTR[err] + else + return nil + end +end + +function get_bool(self, ...) + local val = self:get(...) + return (val == "1" or val == "true" or val == "yes" or val == "on") +end + +function get_first(self, config, stype, option, default) + local rv = default + + self:foreach(config, stype, function(s) + local val = not option and s[".name"] or s[option] + + if type(default) == "number" then + val = tonumber(val) + elseif type(default) == "boolean" then + val = (val == "1" or val == "true" or + val == "yes" or val == "on") + end + + if val ~= nil then + rv = val + return false + end + end) + + return rv +end + +function get_list(self, config, section, option) + if config and section and option then + local val = self:get(config, section, option) + return (type(val) == "table" and val or { val }) + end + return { } +end + + +function section(self, config, stype, name, values) + local rv, err = call("add", { + config = config, + type = stype, + name = name, + values = values + }) + + if type(rv) == "table" then + return rv.section + elseif err then + return false, ERRSTR[err] + else + return nil + end +end + + +function add(self, config, stype) + return self:section(config, stype) +end + +function set(self, config, section, option, ...) + if select('#', ...) == 0 then + local sname, err = self:section(config, option, section) + return (not not sname), err + else + local _, err = call("set", { + config = config, + section = section, + values = { [option] = select(1, ...) } + }) + return (err == nil), ERRSTR[err] + end +end + +function set_list(self, config, section, option, value) + if section == nil or option == nil then + return false + elseif value == nil or (type(value) == "table" and #value == 0) then + return self:delete(config, section, option) + elseif type(value) == "table" then + return self:set(config, section, option, value) + else + return self:set(config, section, option, { value }) + end +end + +function tset(self, config, section, values) + local _, err = call("set", { + config = config, + section = section, + values = values + }) + return (err == nil), ERRSTR[err] +end + +function reorder(self, config, section, index) + local sections + + if type(section) == "string" and type(index) == "number" then + local pos = 0 + + sections = { } + + self:foreach(config, nil, function(s) + if pos == index then + pos = pos + 1 + end + + if s[".name"] ~= section then + pos = pos + 1 + sections[pos] = s[".name"] + else + sections[index + 1] = section + end + end) + elseif type(section) == "table" then + sections = section + else + return false, "Invalid argument" + end + + local _, err = call("order", { + config = config, + sections = sections + }) + + return (err == nil), ERRSTR[err] +end + + +function delete(self, config, section, option) + local _, err = call("delete", { + config = config, + section = section, + option = option + }) + return (err == nil), ERRSTR[err] +end + +function delete_all(self, config, stype, comparator) + local _, err + if type(comparator) == "table" then + _, err = call("delete", { + config = config, + type = stype, + match = comparator + }) + elseif type(comparator) == "function" then + local rv = call("get", { + config = config, + type = stype + }) + + if type(rv) == "table" and type(rv.values) == "table" then + local sname, section + for sname, section in pairs(rv.values) do + if comparator(section) then + _, err = call("delete", { + config = config, + section = sname + }) + end + end + end + elseif comparator == nil then + _, err = call("delete", { + config = config, + type = stype + }) + else + return false, "Invalid argument" + end + + return (err == nil), ERRSTR[err] +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/uci.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/uci.luac new file mode 100644 index 000000000000..9715fe816f36 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/model/uci.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sgi/cgi.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sgi/cgi.lua new file mode 100644 index 000000000000..400db4710d37 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sgi/cgi.lua @@ -0,0 +1,73 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +exectime = os.clock() +module("luci.sgi.cgi", package.seeall) +local ltn12 = require("luci.ltn12") +require("nixio.util") +require("luci.http") +require("luci.sys") +require("luci.dispatcher") + +-- Limited source to avoid endless blocking +local function limitsource(handle, limit) + limit = limit or 0 + local BLOCKSIZE = ltn12.BLOCKSIZE + + return function() + if limit < 1 then + handle:close() + return nil + else + local read = (limit > BLOCKSIZE) and BLOCKSIZE or limit + limit = limit - read + + local chunk = handle:read(read) + if not chunk then handle:close() end + return chunk + end + end +end + +function run() + local r = luci.http.Request( + luci.sys.getenv(), + limitsource(io.stdin, tonumber(luci.sys.getenv("CONTENT_LENGTH"))), + ltn12.sink.file(io.stderr) + ) + + local x = coroutine.create(luci.dispatcher.httpdispatch) + local hcache = "" + local active = true + + while coroutine.status(x) ~= "dead" do + local res, id, data1, data2 = coroutine.resume(x, r) + + if not res then + print("Status: 500 Internal Server Error") + print("Content-Type: text/plain\n") + print(id) + break; + end + + if active then + if id == 1 then + io.write("Status: " .. tostring(data1) .. " " .. data2 .. "\r\n") + elseif id == 2 then + hcache = hcache .. data1 .. ": " .. data2 .. "\r\n" + elseif id == 3 then + io.write(hcache) + io.write("\r\n") + elseif id == 4 then + io.write(tostring(data1 or "")) + elseif id == 5 then + io.flush() + io.close() + active = false + elseif id == 6 then + data1:copyz(nixio.stdout, data2) + data1:close() + end + end + end +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sgi/cgi.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sgi/cgi.luac new file mode 100644 index 000000000000..1375d18adf82 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sgi/cgi.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sgi/uhttpd.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sgi/uhttpd.lua new file mode 100644 index 000000000000..4cd3649c62d8 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sgi/uhttpd.lua @@ -0,0 +1,99 @@ +-- Copyright 2010 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. + +require "nixio.util" +require "luci.http" +require "luci.sys" +require "luci.dispatcher" +require "luci.ltn12" + +function handle_request(env) + exectime = os.clock() + local renv = { + CONTENT_LENGTH = env.CONTENT_LENGTH, + CONTENT_TYPE = env.CONTENT_TYPE, + REQUEST_METHOD = env.REQUEST_METHOD, + REQUEST_URI = env.REQUEST_URI, + PATH_INFO = env.PATH_INFO, + SCRIPT_NAME = env.SCRIPT_NAME:gsub("/+$", ""), + SCRIPT_FILENAME = env.SCRIPT_NAME, + SERVER_PROTOCOL = env.SERVER_PROTOCOL, + QUERY_STRING = env.QUERY_STRING, + DOCUMENT_ROOT = env.DOCUMENT_ROOT, + HTTPS = env.HTTPS, + REDIRECT_STATUS = env.REDIRECT_STATUS, + REMOTE_ADDR = env.REMOTE_ADDR, + REMOTE_NAME = env.REMOTE_NAME, + REMOTE_PORT = env.REMOTE_PORT, + REMOTE_USER = env.REMOTE_USER, + SERVER_ADDR = env.SERVER_ADDR, + SERVER_NAME = env.SERVER_NAME, + SERVER_PORT = env.SERVER_PORT + } + + local k, v + for k, v in pairs(env.headers) do + k = k:upper():gsub("%-", "_") + renv["HTTP_" .. k] = v + end + + local len = tonumber(env.CONTENT_LENGTH) or 0 + local function recv() + if len > 0 then + local rlen, rbuf = uhttpd.recv(4096) + if rlen >= 0 then + len = len - rlen + return rbuf + end + end + return nil + end + + local send = uhttpd.send + + local req = luci.http.Request( + renv, recv, luci.ltn12.sink.file(io.stderr) + ) + + + local x = coroutine.create(luci.dispatcher.httpdispatch) + local hcache = { } + local active = true + + while coroutine.status(x) ~= "dead" do + local res, id, data1, data2 = coroutine.resume(x, req) + + if not res then + send("Status: 500 Internal Server Error\r\n") + send("Content-Type: text/plain\r\n\r\n") + send(tostring(id)) + break + end + + if active then + if id == 1 then + send("Status: ") + send(tostring(data1)) + send(" ") + send(tostring(data2)) + send("\r\n") + elseif id == 2 then + hcache[data1] = data2 + elseif id == 3 then + for k, v in pairs(hcache) do + send(tostring(k)) + send(": ") + send(tostring(v)) + send("\r\n") + end + send("\r\n") + elseif id == 4 then + send(tostring(data1 or "")) + elseif id == 5 then + active = false + elseif id == 6 then + data1:copyz(nixio.stdout, data2) + end + end + end +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sgi/uhttpd.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sgi/uhttpd.luac new file mode 100644 index 000000000000..6b0eb11a48bc Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sgi/uhttpd.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/store.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/store.lua new file mode 100644 index 000000000000..a73598113780 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/store.lua @@ -0,0 +1,6 @@ +-- Copyright 2009 Steven Barth +-- Copyright 2009 Jo-Philipp Wich +-- Licensed to the public under the Apache License 2.0. + +local util = require "luci.util" +module("luci.store", util.threadlocal) diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/store.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/store.luac new file mode 100644 index 000000000000..3e65656ec0b7 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/store.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys.lua new file mode 100644 index 000000000000..e6eb762e48b1 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys.lua @@ -0,0 +1,615 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +local io = require "io" +local os = require "os" +local table = require "table" +local nixio = require "nixio" +local fs = require "nixio.fs" +local uci = require "luci.model.uci" + +local luci = {} +luci.util = require "luci.util" +luci.ip = require "luci.ip" + +local tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select, unpack = + tonumber, ipairs, pairs, pcall, type, next, setmetatable, require, select, unpack + + +module "luci.sys" + +function call(...) + return os.execute(...) / 256 +end + +exec = luci.util.exec + +-- containing the whole environment is returned otherwise this function returns +-- the corresponding string value for the given name or nil if no such variable +-- exists. +getenv = nixio.getenv + +function hostname(newname) + if type(newname) == "string" and #newname > 0 then + fs.writefile( "/proc/sys/kernel/hostname", newname ) + return newname + else + return nixio.uname().nodename + end +end + +function httpget(url, stream, target) + if not target then + local source = stream and io.popen or luci.util.exec + return source("wget -qO- %s" % luci.util.shellquote(url)) + else + return os.execute("wget -qO %s %s" % + {luci.util.shellquote(target), luci.util.shellquote(url)}) + end +end + +function reboot() + return os.execute("reboot >/dev/null 2>&1") +end + +function syslog() + return luci.util.exec("logread") +end + +function dmesg() + return luci.util.exec("dmesg") +end + +function uniqueid(bytes) + local rand = fs.readfile("/dev/urandom", bytes) + return rand and nixio.bin.hexlify(rand) +end + +function uptime() + return nixio.sysinfo().uptime +end + + +net = {} + +local function _nethints(what, callback) + local _, k, e, mac, ip, name, duid, iaid + local cur = uci.cursor() + local ifn = { } + local hosts = { } + local lookup = { } + + local function _add(i, ...) + local k = select(i, ...) + if k then + if not hosts[k] then hosts[k] = { } end + hosts[k][1] = select(1, ...) or hosts[k][1] + hosts[k][2] = select(2, ...) or hosts[k][2] + hosts[k][3] = select(3, ...) or hosts[k][3] + hosts[k][4] = select(4, ...) or hosts[k][4] + end + end + + luci.ip.neighbors(nil, function(neigh) + if neigh.mac and neigh.family == 4 then + _add(what, neigh.mac:string(), neigh.dest:string(), nil, nil) + elseif neigh.mac and neigh.family == 6 then + _add(what, neigh.mac:string(), nil, neigh.dest:string(), nil) + end + end) + + if fs.access("/etc/ethers") then + for e in io.lines("/etc/ethers") do + mac, name = e:match("^([a-fA-F0-9:-]+)%s+(%S+)") + mac = luci.ip.checkmac(mac) + if mac and name then + if luci.ip.checkip4(name) then + _add(what, mac, name, nil, nil) + else + _add(what, mac, nil, nil, name) + end + end + end + end + + cur:foreach("dhcp", "dnsmasq", + function(s) + if s.leasefile and fs.access(s.leasefile) then + for e in io.lines(s.leasefile) do + mac, ip, name = e:match("^%d+ (%S+) (%S+) (%S+)") + mac = luci.ip.checkmac(mac) + if mac and ip then + _add(what, mac, ip, nil, name ~= "*" and name) + end + end + end + end + ) + + cur:foreach("dhcp", "odhcpd", + function(s) + if type(s.leasefile) == "string" and fs.access(s.leasefile) then + for e in io.lines(s.leasefile) do + duid, iaid, name, _, ip = e:match("^# %S+ (%S+) (%S+) (%S+) (-?%d+) %S+ %S+ ([0-9a-f:.]+)/[0-9]+") + mac = net.duid_to_mac(duid) + if mac then + if ip and iaid == "ipv4" then + _add(what, mac, ip, nil, name ~= "*" and name) + elseif ip then + _add(what, mac, nil, ip, name ~= "*" and name) + end + end + end + end + end + ) + + cur:foreach("dhcp", "host", + function(s) + for mac in luci.util.imatch(s.mac) do + mac = luci.ip.checkmac(mac) + if mac then + _add(what, mac, s.ip, nil, s.name) + end + end + end) + + for _, e in ipairs(nixio.getifaddrs()) do + if e.name ~= "lo" then + ifn[e.name] = ifn[e.name] or { } + if e.family == "packet" and e.addr and #e.addr == 17 then + ifn[e.name][1] = e.addr:upper() + elseif e.family == "inet" then + ifn[e.name][2] = e.addr + elseif e.family == "inet6" then + ifn[e.name][3] = e.addr + end + end + end + + for _, e in pairs(ifn) do + if e[what] and (e[2] or e[3]) then + _add(what, e[1], e[2], e[3], e[4]) + end + end + + for _, e in pairs(hosts) do + lookup[#lookup+1] = (what > 1) and e[what] or (e[2] or e[3]) + end + + if #lookup > 0 then + lookup = luci.util.ubus("network.rrdns", "lookup", { + addrs = lookup, + timeout = 250, + limit = 1000 + }) or { } + end + + for _, e in luci.util.kspairs(hosts) do + callback(e[1], e[2], e[3], lookup[e[2]] or lookup[e[3]] or e[4]) + end +end + +-- Each entry contains the values in the following order: +-- [ "mac", "name" ] +function net.mac_hints(callback) + if callback then + _nethints(1, function(mac, v4, v6, name) + name = name or v4 + if name and name ~= mac then + callback(mac, name or v4) + end + end) + else + local rv = { } + _nethints(1, function(mac, v4, v6, name) + name = name or v4 + if name and name ~= mac then + rv[#rv+1] = { mac, name or v4 } + end + end) + return rv + end +end + +-- Each entry contains the values in the following order: +-- [ "ip", "name" ] +function net.ipv4_hints(callback) + if callback then + _nethints(2, function(mac, v4, v6, name) + name = name or mac + if name and name ~= v4 then + callback(v4, name) + end + end) + else + local rv = { } + _nethints(2, function(mac, v4, v6, name) + name = name or mac + if name and name ~= v4 then + rv[#rv+1] = { v4, name } + end + end) + return rv + end +end + +-- Each entry contains the values in the following order: +-- [ "ip", "name" ] +function net.ipv6_hints(callback) + if callback then + _nethints(3, function(mac, v4, v6, name) + name = name or mac + if name and name ~= v6 then + callback(v6, name) + end + end) + else + local rv = { } + _nethints(3, function(mac, v4, v6, name) + name = name or mac + if name and name ~= v6 then + rv[#rv+1] = { v6, name } + end + end) + return rv + end +end + +function net.host_hints(callback) + if callback then + _nethints(1, function(mac, v4, v6, name) + if mac and mac ~= "00:00:00:00:00:00" and (v4 or v6 or name) then + callback(mac, v4, v6, name) + end + end) + else + local rv = { } + _nethints(1, function(mac, v4, v6, name) + if mac and mac ~= "00:00:00:00:00:00" and (v4 or v6 or name) then + local e = { } + if v4 then e.ipv4 = v4 end + if v6 then e.ipv6 = v6 end + if name then e.name = name end + rv[mac] = e + end + end) + return rv + end +end + +function net.conntrack(callback) + local ok, nfct = pcall(io.lines, "/proc/net/nf_conntrack") + if not ok or not nfct then + return nil + end + + local line, connt = nil, (not callback) and { } + for line in nfct do + local fam, l3, l4, rest = + line:match("^(ipv[46]) +(%d+) +%S+ +(%d+) +(.+)$") + + local timeout, tuples = rest:match("^(%d+) +(.+)$") + + if not tuples then + tuples = rest + end + + if fam and l3 and l4 and not tuples:match("^TIME_WAIT ") then + l4 = nixio.getprotobynumber(l4) + + local entry = { + bytes = 0, + packets = 0, + layer3 = fam, + layer4 = l4 and l4.name or "unknown", + timeout = tonumber(timeout, 10) + } + + local key, val + for key, val in tuples:gmatch("(%w+)=(%S+)") do + if key == "bytes" or key == "packets" then + entry[key] = entry[key] + tonumber(val, 10) + elseif key == "src" or key == "dst" then + if entry[key] == nil then + entry[key] = luci.ip.new(val):string() + end + elseif key == "sport" or key == "dport" then + if entry[key] == nil then + entry[key] = val + end + elseif val then + entry[key] = val + end + end + + if callback then + callback(entry) + else + connt[#connt+1] = entry + end + end + end + + return callback and true or connt +end + +function net.devices() + local devs = {} + local seen = {} + for k, v in ipairs(nixio.getifaddrs()) do + if v.name and not seen[v.name] then + seen[v.name] = true + devs[#devs+1] = v.name + end + end + return devs +end + +function net.duid_to_mac(duid) + local b1, b2, b3, b4, b5, b6 + + if type(duid) == "string" then + -- DUID-LLT / Ethernet + if #duid == 28 then + b1, b2, b3, b4, b5, b6 = duid:match("^00010001(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)%x%x%x%x%x%x%x%x$") + + -- DUID-LL / Ethernet + elseif #duid == 20 then + b1, b2, b3, b4, b5, b6 = duid:match("^00030001(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)$") + + -- DUID-LL / Ethernet (Without Header) + elseif #duid == 12 then + b1, b2, b3, b4, b5, b6 = duid:match("^(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)(%x%x)$") + end + end + + return b1 and luci.ip.checkmac(table.concat({ b1, b2, b3, b4, b5, b6 }, ":")) +end + +process = {} + +function process.info(key) + local s = {uid = nixio.getuid(), gid = nixio.getgid()} + return not key and s or s[key] +end + +function process.list() + local data = {} + local k + local ps = luci.util.execi("/bin/busybox top -bn1") + + if not ps then + return + end + + for line in ps do + local pid, ppid, user, stat, vsz, mem, cpu, cmd = line:match( + "^ *(%d+) +(%d+) +(%S.-%S) +([RSDZTW][ 2 then + fd:close() + end +end + +function process.exec(command, stdout, stderr, nowait) + local out_r, out_w, err_r, err_w + if stdout then out_r, out_w = nixio.pipe() end + if stderr then err_r, err_w = nixio.pipe() end + + local pid = nixio.fork() + if pid == 0 then + nixio.chdir("/") + + local null = nixio.open("/dev/null", "w+") + if null then + nixio.dup(out_w or null, nixio.stdout) + nixio.dup(err_w or null, nixio.stderr) + nixio.dup(null, nixio.stdin) + xclose(out_w) + xclose(out_r) + xclose(err_w) + xclose(err_r) + xclose(null) + end + + nixio.exec(unpack(command)) + os.exit(-1) + end + + local _, pfds, rv = nil, {}, { code = -1, pid = pid } + + xclose(out_w) + xclose(err_w) + + if out_r then + pfds[#pfds+1] = { + fd = out_r, + cb = type(stdout) == "function" and stdout, + name = "stdout", + events = nixio.poll_flags("in", "err", "hup") + } + end + + if err_r then + pfds[#pfds+1] = { + fd = err_r, + cb = type(stderr) == "function" and stderr, + name = "stderr", + events = nixio.poll_flags("in", "err", "hup") + } + end + + while #pfds > 0 do + local nfds, err = nixio.poll(pfds, -1) + if not nfds and err ~= nixio.const.EINTR then + break + end + + local i + for i = #pfds, 1, -1 do + local rfd = pfds[i] + if rfd.revents > 0 then + local chunk, err = rfd.fd:read(4096) + if chunk and #chunk > 0 then + if rfd.cb then + rfd.cb(chunk) + else + rfd.buf = rfd.buf or {} + rfd.buf[#rfd.buf + 1] = chunk + end + else + table.remove(pfds, i) + if rfd.buf then + rv[rfd.name] = table.concat(rfd.buf, "") + end + rfd.fd:close() + end + end + end + end + + if not nowait then + _, _, rv.code = nixio.waitpid(pid) + end + + return rv +end + + +user = {} + +-- { "uid", "gid", "name", "passwd", "dir", "shell", "gecos" } +user.getuser = nixio.getpw + +function user.getpasswd(username) + local pwe = nixio.getsp and nixio.getsp(username) or nixio.getpw(username) + local pwh = pwe and (pwe.pwdp or pwe.passwd) + if not pwh or #pwh < 1 then + return nil, pwe + else + return pwh, pwe + end +end + +function user.checkpasswd(username, pass) + local pwh, pwe = user.getpasswd(username) + if pwe then + return (pwh == nil or nixio.crypt(pass, pwh) == pwh) + end + return false +end + +function user.setpasswd(username, password) + return os.execute("(echo %s; sleep 1; echo %s) | passwd %s >/dev/null 2>&1" %{ + luci.util.shellquote(password), + luci.util.shellquote(password), + luci.util.shellquote(username) + }) +end + + +wifi = {} + +function wifi.getiwinfo(ifname) + local ntm = require "luci.model.network" + + ntm.init() + + local wnet = ntm:get_wifinet(ifname) + if wnet and wnet.iwinfo then + return wnet.iwinfo + end + + local wdev = ntm:get_wifidev(ifname) + if wdev and wdev.iwinfo then + return wdev.iwinfo + end + + return { ifname = ifname } +end + + +init = {} +init.dir = "/etc/init.d/" + +function init.names() + local names = { } + for name in fs.glob(init.dir.."*") do + names[#names+1] = fs.basename(name) + end + return names +end + +function init.index(name) + name = fs.basename(name) + if fs.access(init.dir..name) then + return call("env -i sh -c 'source %s%s enabled; exit ${START:-255}' >/dev/null" + %{ init.dir, name }) + end +end + +local function init_action(action, name) + name = fs.basename(name) + if fs.access(init.dir..name) then + return call("env -i %s%s %s >/dev/null" %{ init.dir, name, action }) + end +end + +function init.enabled(name) + return (init_action("enabled", name) == 0) +end + +function init.enable(name) + return (init_action("enable", name) == 0) +end + +function init.disable(name) + return (init_action("disable", name) == 0) +end + +function init.start(name) + return (init_action("start", name) == 0) +end + +function init.stop(name) + return (init_action("stop", name) == 0) +end + +function init.restart(name) + return (init_action("restart", name) == 0) +end + +function init.reload(name) + return (init_action("reload", name) == 0) +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys.luac new file mode 100644 index 000000000000..ebe07d771fd9 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo.lua new file mode 100644 index 000000000000..aa054a246f3b --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo.lua @@ -0,0 +1,19 @@ +-- Licensed to the public under the Apache License 2.0. + +local setmetatable, require, rawget, rawset = setmetatable, require, rawget, rawset + +module "luci.sys.zoneinfo" + +setmetatable(_M, { + __index = function(t, k) + if k == "TZ" and not rawget(t, k) then + local m = require "luci.sys.zoneinfo.tzdata" + rawset(t, k, rawget(m, k)) + elseif k == "OFFSET" and not rawget(t, k) then + local m = require "luci.sys.zoneinfo.tzoffset" + rawset(t, k, rawget(m, k)) + end + + return rawget(t, k) + end +}) diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo.luac new file mode 100644 index 000000000000..06ad32d826b6 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo/tzdata.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo/tzdata.lua new file mode 100644 index 000000000000..a3edbf5cb49a --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo/tzdata.lua @@ -0,0 +1,457 @@ +-- Licensed to the public under the Apache License 2.0. + +module "luci.sys.zoneinfo.tzdata" + +TZ = { + { 'Africa/Abidjan', 'GMT0' }, + { 'Africa/Accra', 'GMT0' }, + { 'Africa/Addis Ababa', 'EAT-3' }, + { 'Africa/Algiers', 'CET-1' }, + { 'Africa/Asmara', 'EAT-3' }, + { 'Africa/Bamako', 'GMT0' }, + { 'Africa/Bangui', 'WAT-1' }, + { 'Africa/Banjul', 'GMT0' }, + { 'Africa/Bissau', 'GMT0' }, + { 'Africa/Blantyre', 'CAT-2' }, + { 'Africa/Brazzaville', 'WAT-1' }, + { 'Africa/Bujumbura', 'CAT-2' }, + { 'Africa/Cairo', 'EET-2' }, + { 'Africa/Casablanca', '<+01>-1' }, + { 'Africa/Ceuta', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Africa/Conakry', 'GMT0' }, + { 'Africa/Dakar', 'GMT0' }, + { 'Africa/Dar es Salaam', 'EAT-3' }, + { 'Africa/Djibouti', 'EAT-3' }, + { 'Africa/Douala', 'WAT-1' }, + { 'Africa/El Aaiun', '<+01>-1' }, + { 'Africa/Freetown', 'GMT0' }, + { 'Africa/Gaborone', 'CAT-2' }, + { 'Africa/Harare', 'CAT-2' }, + { 'Africa/Johannesburg', 'SAST-2' }, + { 'Africa/Juba', 'CAT-2' }, + { 'Africa/Kampala', 'EAT-3' }, + { 'Africa/Khartoum', 'CAT-2' }, + { 'Africa/Kigali', 'CAT-2' }, + { 'Africa/Kinshasa', 'WAT-1' }, + { 'Africa/Lagos', 'WAT-1' }, + { 'Africa/Libreville', 'WAT-1' }, + { 'Africa/Lome', 'GMT0' }, + { 'Africa/Luanda', 'WAT-1' }, + { 'Africa/Lubumbashi', 'CAT-2' }, + { 'Africa/Lusaka', 'CAT-2' }, + { 'Africa/Malabo', 'WAT-1' }, + { 'Africa/Maputo', 'CAT-2' }, + { 'Africa/Maseru', 'SAST-2' }, + { 'Africa/Mbabane', 'SAST-2' }, + { 'Africa/Mogadishu', 'EAT-3' }, + { 'Africa/Monrovia', 'GMT0' }, + { 'Africa/Nairobi', 'EAT-3' }, + { 'Africa/Ndjamena', 'WAT-1' }, + { 'Africa/Niamey', 'WAT-1' }, + { 'Africa/Nouakchott', 'GMT0' }, + { 'Africa/Ouagadougou', 'GMT0' }, + { 'Africa/Porto-Novo', 'WAT-1' }, + { 'Africa/Sao Tome', 'GMT0' }, + { 'Africa/Tripoli', 'EET-2' }, + { 'Africa/Tunis', 'CET-1' }, + { 'Africa/Windhoek', 'CAT-2' }, + { 'America/Adak', 'HST10HDT,M3.2.0,M11.1.0' }, + { 'America/Anchorage', 'AKST9AKDT,M3.2.0,M11.1.0' }, + { 'America/Anguilla', 'AST4' }, + { 'America/Antigua', 'AST4' }, + { 'America/Araguaina', '<-03>3' }, + { 'America/Argentina/Buenos Aires', '<-03>3' }, + { 'America/Argentina/Catamarca', '<-03>3' }, + { 'America/Argentina/Cordoba', '<-03>3' }, + { 'America/Argentina/Jujuy', '<-03>3' }, + { 'America/Argentina/La Rioja', '<-03>3' }, + { 'America/Argentina/Mendoza', '<-03>3' }, + { 'America/Argentina/Rio Gallegos', '<-03>3' }, + { 'America/Argentina/Salta', '<-03>3' }, + { 'America/Argentina/San Juan', '<-03>3' }, + { 'America/Argentina/San Luis', '<-03>3' }, + { 'America/Argentina/Tucuman', '<-03>3' }, + { 'America/Argentina/Ushuaia', '<-03>3' }, + { 'America/Aruba', 'AST4' }, + { 'America/Asuncion', '<-04>4<-03>,M10.1.0/0,M3.4.0/0' }, + { 'America/Atikokan', 'EST5' }, + { 'America/Bahia', '<-03>3' }, + { 'America/Bahia Banderas', 'CST6CDT,M4.1.0,M10.5.0' }, + { 'America/Barbados', 'AST4' }, + { 'America/Belem', '<-03>3' }, + { 'America/Belize', 'CST6' }, + { 'America/Blanc-Sablon', 'AST4' }, + { 'America/Boa Vista', '<-04>4' }, + { 'America/Bogota', '<-05>5' }, + { 'America/Boise', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'America/Cambridge Bay', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'America/Campo Grande', '<-04>4' }, + { 'America/Cancun', 'EST5' }, + { 'America/Caracas', '<-04>4' }, + { 'America/Cayenne', '<-03>3' }, + { 'America/Cayman', 'EST5' }, + { 'America/Chicago', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Chihuahua', 'MST7MDT,M4.1.0,M10.5.0' }, + { 'America/Costa Rica', 'CST6' }, + { 'America/Creston', 'MST7' }, + { 'America/Cuiaba', '<-04>4' }, + { 'America/Curacao', 'AST4' }, + { 'America/Danmarkshavn', 'GMT0' }, + { 'America/Dawson', 'MST7' }, + { 'America/Dawson Creek', 'MST7' }, + { 'America/Denver', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'America/Detroit', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Dominica', 'AST4' }, + { 'America/Edmonton', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'America/Eirunepe', '<-05>5' }, + { 'America/El Salvador', 'CST6' }, + { 'America/Fort Nelson', 'MST7' }, + { 'America/Fortaleza', '<-03>3' }, + { 'America/Glace Bay', 'AST4ADT,M3.2.0,M11.1.0' }, + { 'America/Goose Bay', 'AST4ADT,M3.2.0,M11.1.0' }, + { 'America/Grand Turk', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Grenada', 'AST4' }, + { 'America/Guadeloupe', 'AST4' }, + { 'America/Guatemala', 'CST6' }, + { 'America/Guayaquil', '<-05>5' }, + { 'America/Guyana', '<-04>4' }, + { 'America/Halifax', 'AST4ADT,M3.2.0,M11.1.0' }, + { 'America/Havana', 'CST5CDT,M3.2.0/0,M11.1.0/1' }, + { 'America/Hermosillo', 'MST7' }, + { 'America/Indiana/Indianapolis', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Indiana/Knox', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Indiana/Marengo', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Indiana/Petersburg', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Indiana/Tell City', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Indiana/Vevay', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Indiana/Vincennes', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Indiana/Winamac', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Inuvik', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'America/Iqaluit', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Jamaica', 'EST5' }, + { 'America/Juneau', 'AKST9AKDT,M3.2.0,M11.1.0' }, + { 'America/Kentucky/Louisville', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Kentucky/Monticello', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Kralendijk', 'AST4' }, + { 'America/La Paz', '<-04>4' }, + { 'America/Lima', '<-05>5' }, + { 'America/Los Angeles', 'PST8PDT,M3.2.0,M11.1.0' }, + { 'America/Lower Princes', 'AST4' }, + { 'America/Maceio', '<-03>3' }, + { 'America/Managua', 'CST6' }, + { 'America/Manaus', '<-04>4' }, + { 'America/Marigot', 'AST4' }, + { 'America/Martinique', 'AST4' }, + { 'America/Matamoros', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Mazatlan', 'MST7MDT,M4.1.0,M10.5.0' }, + { 'America/Menominee', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Merida', 'CST6CDT,M4.1.0,M10.5.0' }, + { 'America/Metlakatla', 'AKST9AKDT,M3.2.0,M11.1.0' }, + { 'America/Mexico City', 'CST6CDT,M4.1.0,M10.5.0' }, + { 'America/Miquelon', '<-03>3<-02>,M3.2.0,M11.1.0' }, + { 'America/Moncton', 'AST4ADT,M3.2.0,M11.1.0' }, + { 'America/Monterrey', 'CST6CDT,M4.1.0,M10.5.0' }, + { 'America/Montevideo', '<-03>3' }, + { 'America/Montserrat', 'AST4' }, + { 'America/Nassau', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/New York', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Nipigon', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Nome', 'AKST9AKDT,M3.2.0,M11.1.0' }, + { 'America/Noronha', '<-02>2' }, + { 'America/North Dakota/Beulah', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/North Dakota/Center', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/North Dakota/New Salem', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Nuuk', '<-03>3<-02>,M3.5.0/-2,M10.5.0/-1' }, + { 'America/Ojinaga', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'America/Panama', 'EST5' }, + { 'America/Pangnirtung', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Paramaribo', '<-03>3' }, + { 'America/Phoenix', 'MST7' }, + { 'America/Port of Spain', 'AST4' }, + { 'America/Port-au-Prince', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Porto Velho', '<-04>4' }, + { 'America/Puerto Rico', 'AST4' }, + { 'America/Punta Arenas', '<-03>3' }, + { 'America/Rainy River', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Rankin Inlet', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Recife', '<-03>3' }, + { 'America/Regina', 'CST6' }, + { 'America/Resolute', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Rio Branco', '<-05>5' }, + { 'America/Santarem', '<-03>3' }, + { 'America/Santiago', '<-04>4<-03>,M9.1.6/24,M4.1.6/24' }, + { 'America/Santo Domingo', 'AST4' }, + { 'America/Sao Paulo', '<-03>3' }, + { 'America/Scoresbysund', '<-01>1<+00>,M3.5.0/0,M10.5.0/1' }, + { 'America/Sitka', 'AKST9AKDT,M3.2.0,M11.1.0' }, + { 'America/St Barthelemy', 'AST4' }, + { 'America/St Johns', 'NST3:30NDT,M3.2.0,M11.1.0' }, + { 'America/St Kitts', 'AST4' }, + { 'America/St Lucia', 'AST4' }, + { 'America/St Thomas', 'AST4' }, + { 'America/St Vincent', 'AST4' }, + { 'America/Swift Current', 'CST6' }, + { 'America/Tegucigalpa', 'CST6' }, + { 'America/Thule', 'AST4ADT,M3.2.0,M11.1.0' }, + { 'America/Thunder Bay', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Tijuana', 'PST8PDT,M3.2.0,M11.1.0' }, + { 'America/Toronto', 'EST5EDT,M3.2.0,M11.1.0' }, + { 'America/Tortola', 'AST4' }, + { 'America/Vancouver', 'PST8PDT,M3.2.0,M11.1.0' }, + { 'America/Whitehorse', 'MST7' }, + { 'America/Winnipeg', 'CST6CDT,M3.2.0,M11.1.0' }, + { 'America/Yakutat', 'AKST9AKDT,M3.2.0,M11.1.0' }, + { 'America/Yellowknife', 'MST7MDT,M3.2.0,M11.1.0' }, + { 'Antarctica/Casey', '<+11>-11' }, + { 'Antarctica/Davis', '<+07>-7' }, + { 'Antarctica/DumontDUrville', '<+10>-10' }, + { 'Antarctica/Macquarie', 'AEST-10AEDT,M10.1.0,M4.1.0/3' }, + { 'Antarctica/Mawson', '<+05>-5' }, + { 'Antarctica/McMurdo', 'NZST-12NZDT,M9.5.0,M4.1.0/3' }, + { 'Antarctica/Palmer', '<-03>3' }, + { 'Antarctica/Rothera', '<-03>3' }, + { 'Antarctica/Syowa', '<+03>-3' }, + { 'Antarctica/Troll', '<+00>0<+02>-2,M3.5.0/1,M10.5.0/3' }, + { 'Antarctica/Vostok', '<+06>-6' }, + { 'Arctic/Longyearbyen', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Asia/Aden', '<+03>-3' }, + { 'Asia/Almaty', '<+06>-6' }, + { 'Asia/Amman', 'EET-2EEST,M2.5.4/24,M10.5.5/1' }, + { 'Asia/Anadyr', '<+12>-12' }, + { 'Asia/Aqtau', '<+05>-5' }, + { 'Asia/Aqtobe', '<+05>-5' }, + { 'Asia/Ashgabat', '<+05>-5' }, + { 'Asia/Atyrau', '<+05>-5' }, + { 'Asia/Baghdad', '<+03>-3' }, + { 'Asia/Bahrain', '<+03>-3' }, + { 'Asia/Baku', '<+04>-4' }, + { 'Asia/Bangkok', '<+07>-7' }, + { 'Asia/Barnaul', '<+07>-7' }, + { 'Asia/Beirut', 'EET-2EEST,M3.5.0/0,M10.5.0/0' }, + { 'Asia/Bishkek', '<+06>-6' }, + { 'Asia/Brunei', '<+08>-8' }, + { 'Asia/Chita', '<+09>-9' }, + { 'Asia/Choibalsan', '<+08>-8' }, + { 'Asia/Colombo', '<+0530>-5:30' }, + { 'Asia/Damascus', 'EET-2EEST,M3.5.5/0,M10.5.5/0' }, + { 'Asia/Dhaka', '<+06>-6' }, + { 'Asia/Dili', '<+09>-9' }, + { 'Asia/Dubai', '<+04>-4' }, + { 'Asia/Dushanbe', '<+05>-5' }, + { 'Asia/Famagusta', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Asia/Gaza', 'EET-2EEST,M3.4.4/72,M10.4.4/25' }, + { 'Asia/Hebron', 'EET-2EEST,M3.4.4/72,M10.4.4/25' }, + { 'Asia/Ho Chi Minh', '<+07>-7' }, + { 'Asia/Hong Kong', 'HKT-8' }, + { 'Asia/Hovd', '<+07>-7' }, + { 'Asia/Irkutsk', '<+08>-8' }, + { 'Asia/Jakarta', 'WIB-7' }, + { 'Asia/Jayapura', 'WIT-9' }, + { 'Asia/Jerusalem', 'IST-2IDT,M3.4.4/26,M10.5.0' }, + { 'Asia/Kabul', '<+0430>-4:30' }, + { 'Asia/Kamchatka', '<+12>-12' }, + { 'Asia/Karachi', 'PKT-5' }, + { 'Asia/Kathmandu', '<+0545>-5:45' }, + { 'Asia/Khandyga', '<+09>-9' }, + { 'Asia/Kolkata', 'IST-5:30' }, + { 'Asia/Krasnoyarsk', '<+07>-7' }, + { 'Asia/Kuala Lumpur', '<+08>-8' }, + { 'Asia/Kuching', '<+08>-8' }, + { 'Asia/Kuwait', '<+03>-3' }, + { 'Asia/Macau', 'CST-8' }, + { 'Asia/Magadan', '<+11>-11' }, + { 'Asia/Makassar', 'WITA-8' }, + { 'Asia/Manila', 'PST-8' }, + { 'Asia/Muscat', '<+04>-4' }, + { 'Asia/Nicosia', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Asia/Novokuznetsk', '<+07>-7' }, + { 'Asia/Novosibirsk', '<+07>-7' }, + { 'Asia/Omsk', '<+06>-6' }, + { 'Asia/Oral', '<+05>-5' }, + { 'Asia/Phnom Penh', '<+07>-7' }, + { 'Asia/Pontianak', 'WIB-7' }, + { 'Asia/Pyongyang', 'KST-9' }, + { 'Asia/Qatar', '<+03>-3' }, + { 'Asia/Qostanay', '<+06>-6' }, + { 'Asia/Qyzylorda', '<+05>-5' }, + { 'Asia/Riyadh', '<+03>-3' }, + { 'Asia/Sakhalin', '<+11>-11' }, + { 'Asia/Samarkand', '<+05>-5' }, + { 'Asia/Seoul', 'KST-9' }, + { 'Asia/Shanghai', 'CST-8' }, + { 'Asia/Singapore', '<+08>-8' }, + { 'Asia/Srednekolymsk', '<+11>-11' }, + { 'Asia/Taipei', 'CST-8' }, + { 'Asia/Tashkent', '<+05>-5' }, + { 'Asia/Tbilisi', '<+04>-4' }, + { 'Asia/Tehran', '<+0330>-3:30' }, + { 'Asia/Thimphu', '<+06>-6' }, + { 'Asia/Tokyo', 'JST-9' }, + { 'Asia/Tomsk', '<+07>-7' }, + { 'Asia/Ulaanbaatar', '<+08>-8' }, + { 'Asia/Urumqi', '<+06>-6' }, + { 'Asia/Ust-Nera', '<+10>-10' }, + { 'Asia/Vientiane', '<+07>-7' }, + { 'Asia/Vladivostok', '<+10>-10' }, + { 'Asia/Yakutsk', '<+09>-9' }, + { 'Asia/Yangon', '<+0630>-6:30' }, + { 'Asia/Yekaterinburg', '<+05>-5' }, + { 'Asia/Yerevan', '<+04>-4' }, + { 'Atlantic/Azores', '<-01>1<+00>,M3.5.0/0,M10.5.0/1' }, + { 'Atlantic/Bermuda', 'AST4ADT,M3.2.0,M11.1.0' }, + { 'Atlantic/Canary', 'WET0WEST,M3.5.0/1,M10.5.0' }, + { 'Atlantic/Cape Verde', '<-01>1' }, + { 'Atlantic/Faroe', 'WET0WEST,M3.5.0/1,M10.5.0' }, + { 'Atlantic/Madeira', 'WET0WEST,M3.5.0/1,M10.5.0' }, + { 'Atlantic/Reykjavik', 'GMT0' }, + { 'Atlantic/South Georgia', '<-02>2' }, + { 'Atlantic/St Helena', 'GMT0' }, + { 'Atlantic/Stanley', '<-03>3' }, + { 'Australia/Adelaide', 'ACST-9:30ACDT,M10.1.0,M4.1.0/3' }, + { 'Australia/Brisbane', 'AEST-10' }, + { 'Australia/Broken Hill', 'ACST-9:30ACDT,M10.1.0,M4.1.0/3' }, + { 'Australia/Darwin', 'ACST-9:30' }, + { 'Australia/Eucla', '<+0845>-8:45' }, + { 'Australia/Hobart', 'AEST-10AEDT,M10.1.0,M4.1.0/3' }, + { 'Australia/Lindeman', 'AEST-10' }, + { 'Australia/Lord Howe', '<+1030>-10:30<+11>-11,M10.1.0,M4.1.0' }, + { 'Australia/Melbourne', 'AEST-10AEDT,M10.1.0,M4.1.0/3' }, + { 'Australia/Perth', 'AWST-8' }, + { 'Australia/Sydney', 'AEST-10AEDT,M10.1.0,M4.1.0/3' }, + { 'Etc/GMT', 'GMT0' }, + { 'Etc/GMT+1', '<-01>1' }, + { 'Etc/GMT+10', '<-10>10' }, + { 'Etc/GMT+11', '<-11>11' }, + { 'Etc/GMT+12', '<-12>12' }, + { 'Etc/GMT+2', '<-02>2' }, + { 'Etc/GMT+3', '<-03>3' }, + { 'Etc/GMT+4', '<-04>4' }, + { 'Etc/GMT+5', '<-05>5' }, + { 'Etc/GMT+6', '<-06>6' }, + { 'Etc/GMT+7', '<-07>7' }, + { 'Etc/GMT+8', '<-08>8' }, + { 'Etc/GMT+9', '<-09>9' }, + { 'Etc/GMT-1', '<+01>-1' }, + { 'Etc/GMT-10', '<+10>-10' }, + { 'Etc/GMT-11', '<+11>-11' }, + { 'Etc/GMT-12', '<+12>-12' }, + { 'Etc/GMT-13', '<+13>-13' }, + { 'Etc/GMT-14', '<+14>-14' }, + { 'Etc/GMT-2', '<+02>-2' }, + { 'Etc/GMT-3', '<+03>-3' }, + { 'Etc/GMT-4', '<+04>-4' }, + { 'Etc/GMT-5', '<+05>-5' }, + { 'Etc/GMT-6', '<+06>-6' }, + { 'Etc/GMT-7', '<+07>-7' }, + { 'Etc/GMT-8', '<+08>-8' }, + { 'Etc/GMT-9', '<+09>-9' }, + { 'Europe/Amsterdam', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Andorra', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Astrakhan', '<+04>-4' }, + { 'Europe/Athens', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Belgrade', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Berlin', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Bratislava', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Brussels', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Bucharest', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Budapest', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Busingen', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Chisinau', 'EET-2EEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Copenhagen', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Dublin', 'IST-1GMT0,M10.5.0,M3.5.0/1' }, + { 'Europe/Gibraltar', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Guernsey', 'GMT0BST,M3.5.0/1,M10.5.0' }, + { 'Europe/Helsinki', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Isle of Man', 'GMT0BST,M3.5.0/1,M10.5.0' }, + { 'Europe/Istanbul', '<+03>-3' }, + { 'Europe/Jersey', 'GMT0BST,M3.5.0/1,M10.5.0' }, + { 'Europe/Kaliningrad', 'EET-2' }, + { 'Europe/Kirov', '<+03>-3' }, + { 'Europe/Kyiv', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Lisbon', 'WET0WEST,M3.5.0/1,M10.5.0' }, + { 'Europe/Ljubljana', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/London', 'GMT0BST,M3.5.0/1,M10.5.0' }, + { 'Europe/Luxembourg', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Madrid', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Malta', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Mariehamn', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Minsk', '<+03>-3' }, + { 'Europe/Monaco', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Moscow', 'MSK-3' }, + { 'Europe/Oslo', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Paris', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Podgorica', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Prague', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Riga', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Rome', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Samara', '<+04>-4' }, + { 'Europe/San Marino', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Sarajevo', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Saratov', '<+04>-4' }, + { 'Europe/Simferopol', 'MSK-3' }, + { 'Europe/Skopje', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Sofia', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Stockholm', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Tallinn', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Tirane', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Ulyanovsk', '<+04>-4' }, + { 'Europe/Uzhgorod', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Vaduz', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Vatican', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Vienna', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Vilnius', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Volgograd', '<+03>-3' }, + { 'Europe/Warsaw', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Zagreb', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Europe/Zaporozhye', 'EET-2EEST,M3.5.0/3,M10.5.0/4' }, + { 'Europe/Zurich', 'CET-1CEST,M3.5.0,M10.5.0/3' }, + { 'Indian/Antananarivo', 'EAT-3' }, + { 'Indian/Chagos', '<+06>-6' }, + { 'Indian/Christmas', '<+07>-7' }, + { 'Indian/Cocos', '<+0630>-6:30' }, + { 'Indian/Comoro', 'EAT-3' }, + { 'Indian/Kerguelen', '<+05>-5' }, + { 'Indian/Mahe', '<+04>-4' }, + { 'Indian/Maldives', '<+05>-5' }, + { 'Indian/Mauritius', '<+04>-4' }, + { 'Indian/Mayotte', 'EAT-3' }, + { 'Indian/Reunion', '<+04>-4' }, + { 'Pacific/Apia', '<+13>-13' }, + { 'Pacific/Auckland', 'NZST-12NZDT,M9.5.0,M4.1.0/3' }, + { 'Pacific/Bougainville', '<+11>-11' }, + { 'Pacific/Chatham', '<+1245>-12:45<+1345>,M9.5.0/2:45,M4.1.0/3:45' }, + { 'Pacific/Chuuk', '<+10>-10' }, + { 'Pacific/Easter', '<-06>6<-05>,M9.1.6/22,M4.1.6/22' }, + { 'Pacific/Efate', '<+11>-11' }, + { 'Pacific/Fakaofo', '<+13>-13' }, + { 'Pacific/Fiji', '<+12>-12<+13>,M11.2.0,M1.2.3/99' }, + { 'Pacific/Funafuti', '<+12>-12' }, + { 'Pacific/Galapagos', '<-06>6' }, + { 'Pacific/Gambier', '<-09>9' }, + { 'Pacific/Guadalcanal', '<+11>-11' }, + { 'Pacific/Guam', 'ChST-10' }, + { 'Pacific/Honolulu', 'HST10' }, + { 'Pacific/Kanton', '<+13>-13' }, + { 'Pacific/Kiritimati', '<+14>-14' }, + { 'Pacific/Kosrae', '<+11>-11' }, + { 'Pacific/Kwajalein', '<+12>-12' }, + { 'Pacific/Majuro', '<+12>-12' }, + { 'Pacific/Marquesas', '<-0930>9:30' }, + { 'Pacific/Midway', 'SST11' }, + { 'Pacific/Nauru', '<+12>-12' }, + { 'Pacific/Niue', '<-11>11' }, + { 'Pacific/Norfolk', '<+11>-11<+12>,M10.1.0,M4.1.0/3' }, + { 'Pacific/Noumea', '<+11>-11' }, + { 'Pacific/Pago Pago', 'SST11' }, + { 'Pacific/Palau', '<+09>-9' }, + { 'Pacific/Pitcairn', '<-08>8' }, + { 'Pacific/Pohnpei', '<+11>-11' }, + { 'Pacific/Port Moresby', '<+10>-10' }, + { 'Pacific/Rarotonga', '<-10>10' }, + { 'Pacific/Saipan', 'ChST-10' }, + { 'Pacific/Tahiti', '<-10>10' }, + { 'Pacific/Tarawa', '<+12>-12' }, + { 'Pacific/Tongatapu', '<+13>-13' }, + { 'Pacific/Wake', '<+12>-12' }, + { 'Pacific/Wallis', '<+12>-12' }, +} diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo/tzdata.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo/tzdata.luac new file mode 100644 index 000000000000..4391f64ec7fb Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo/tzdata.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo/tzoffset.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo/tzoffset.lua new file mode 100644 index 000000000000..caee1d2c1c78 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo/tzoffset.lua @@ -0,0 +1,46 @@ +-- Licensed to the public under the Apache License 2.0. + +module "luci.sys.zoneinfo.tzoffset" + +OFFSET = { + gmt = 0, -- GMT + eat = 10800, -- EAT + cet = 3600, -- CET + wat = 3600, -- WAT + cat = 7200, -- CAT + eet = 7200, -- EET + sast = 7200, -- SAST + hst = -36000, -- HST + hdt = -32400, -- HDT + akst = -32400, -- AKST + akdt = -28800, -- AKDT + ast = -14400, -- AST + est = -18000, -- EST + cst = -21600, -- CST + cdt = -18000, -- CDT + mst = -25200, -- MST + mdt = -21600, -- MDT + pst = -28800, -- PST + pdt = -25200, -- PDT + nst = -12600, -- NST + ndt = -9000, -- NDT + aest = 36000, -- AEST + aedt = 39600, -- AEDT + nzst = 43200, -- NZST + nzdt = 46800, -- NZDT + hkt = 28800, -- HKT + wib = 25200, -- WIB + wit = 32400, -- WIT + ist = 7200, -- IST + idt = 10800, -- IDT + pkt = 18000, -- PKT + wita = 28800, -- WITA + kst = 32400, -- KST + jst = 32400, -- JST + wet = 0, -- WET + acst = 34200, -- ACST + acdt = 37800, -- ACDT + awst = 28800, -- AWST + msk = 10800, -- MSK + sst = -39600, -- SST +} diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo/tzoffset.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo/tzoffset.luac new file mode 100644 index 000000000000..2e887a090669 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/sys/zoneinfo/tzoffset.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/template.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/template.lua new file mode 100644 index 000000000000..3955bd76f3a7 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/template.lua @@ -0,0 +1,100 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +local util = require "luci.util" +local config = require "luci.config" +local tparser = require "luci.template.parser" + +local tostring, pairs, loadstring = tostring, pairs, loadstring +local setmetatable, loadfile = setmetatable, loadfile +local getfenv, setfenv, rawget = getfenv, setfenv, rawget +local assert, type, error = assert, type, error + +--- LuCI template library. +module "luci.template" + +config.template = config.template or {} +viewdir = config.template.viewdir or util.libpath() .. "/view" + + +-- Define the namespace for template modules +context = util.threadlocal() + +--- Render a certain template. +-- @param name Template name +-- @param scope Scope to assign to template (optional) +function render(name, scope) + return Template(name):render(scope or getfenv(2)) +end + +--- Render a template from a string. +-- @param template Template string +-- @param scope Scope to assign to template (optional) +function render_string(template, scope) + return Template(nil, template):render(scope or getfenv(2)) +end + + +-- Template class +Template = util.class() + +-- Shared template cache to store templates in to avoid unnecessary reloading +Template.cache = setmetatable({}, {__mode = "v"}) + + +-- Constructor - Reads and compiles the template on-demand +function Template.__init__(self, name, template) + if name then + self.template = self.cache[name] + self.name = name + else + self.name = "[string]" + end + + -- Create a new namespace for this template + self.viewns = context.viewns + + -- If we have a cached template, skip compiling and loading + if not self.template then + + -- Compile template + local err + local sourcefile + + if name then + sourcefile = viewdir .. "/" .. name .. ".htm" + self.template, _, err = tparser.parse(sourcefile) + else + sourcefile = "[string]" + self.template, _, err = tparser.parse_string(template) + end + + -- If we have no valid template throw error, otherwise cache the template + if not self.template then + error("Failed to load template '" .. self.name .. "'.\n" .. + "Error while parsing template '" .. sourcefile .. "':\n" .. + (err or "Unknown syntax error")) + elseif name then + self.cache[name] = self.template + end + end +end + + +-- Renders a template +function Template.render(self, scope) + scope = scope or getfenv(2) + + -- Put our predefined objects in the scope of the template + setfenv(self.template, setmetatable({}, {__index = + function(tbl, key) + return rawget(tbl, key) or self.viewns[key] or scope[key] + end})) + + -- Now finally render the thing + local stat, err = util.copcall(self.template) + if not stat then + error("Failed to execute template '" .. self.name .. "'.\n" .. + "A runtime error occurred: " .. tostring(err or "(nil)")) + end +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/template.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/template.luac new file mode 100644 index 000000000000..2c677e819521 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/template.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/util.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/util.lua new file mode 100644 index 000000000000..89757917ff65 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/util.lua @@ -0,0 +1,782 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +local io = require "io" +local math = require "math" +local table = require "table" +local debug = require "debug" +local ldebug = require "luci.debug" +local string = require "string" +local coroutine = require "coroutine" +local tparser = require "luci.template.parser" +local json = require "luci.jsonc" +local lhttp = require "lucihttp" + +local _ubus = require "ubus" +local _ubus_connection = nil + +local getmetatable, setmetatable = getmetatable, setmetatable +local rawget, rawset, unpack, select = rawget, rawset, unpack, select +local tostring, type, assert, error = tostring, type, assert, error +local ipairs, pairs, next, loadstring = ipairs, pairs, next, loadstring +local require, pcall, xpcall = require, pcall, xpcall +local collectgarbage, get_memory_limit = collectgarbage, get_memory_limit + +module "luci.util" + +-- +-- Pythonic string formatting extension +-- +getmetatable("").__mod = function(a, b) + local ok, res + + if not b then + return a + elseif type(b) == "table" then + local k, _ + for k, _ in pairs(b) do if type(b[k]) == "userdata" then b[k] = tostring(b[k]) end end + + ok, res = pcall(a.format, a, unpack(b)) + if not ok then + error(res, 2) + end + return res + else + if type(b) == "userdata" then b = tostring(b) end + + ok, res = pcall(a.format, a, b) + if not ok then + error(res, 2) + end + return res + end +end + + +-- +-- Class helper routines +-- + +-- Instantiates a class +local function _instantiate(class, ...) + local inst = setmetatable({}, {__index = class}) + + if inst.__init__ then + inst:__init__(...) + end + + return inst +end + +-- The class object can be instantiated by calling itself. +-- Any class functions or shared parameters can be attached to this object. +-- Attaching a table to the class object makes this table shared between +-- all instances of this class. For object parameters use the __init__ function. +-- Classes can inherit member functions and values from a base class. +-- Class can be instantiated by calling them. All parameters will be passed +-- to the __init__ function of this class - if such a function exists. +-- The __init__ function must be used to set any object parameters that are not shared +-- with other objects of this class. Any return values will be ignored. +function class(base) + return setmetatable({}, { + __call = _instantiate, + __index = base + }) +end + +function instanceof(object, class) + local meta = getmetatable(object) + while meta and meta.__index do + if meta.__index == class then + return true + end + meta = getmetatable(meta.__index) + end + return false +end + + +-- +-- Scope manipulation routines +-- + +coxpt = setmetatable({}, { __mode = "kv" }) + +local tl_meta = { + __mode = "k", + + __index = function(self, key) + local t = rawget(self, coxpt[coroutine.running()] + or coroutine.running() or 0) + return t and t[key] + end, + + __newindex = function(self, key, value) + local c = coxpt[coroutine.running()] or coroutine.running() or 0 + local r = rawget(self, c) + if not r then + rawset(self, c, { [key] = value }) + else + r[key] = value + end + end +} + +-- the current active coroutine. A thread local store is private a table object +-- whose values can't be accessed from outside of the running coroutine. +function threadlocal(tbl) + return setmetatable(tbl or {}, tl_meta) +end + + +-- +-- Debugging routines +-- + +function perror(obj) + return io.stderr:write(tostring(obj) .. "\n") +end + +function dumptable(t, maxdepth, i, seen) + i = i or 0 + seen = seen or setmetatable({}, {__mode="k"}) + + for k,v in pairs(t) do + perror(string.rep("\t", i) .. tostring(k) .. "\t" .. tostring(v)) + if type(v) == "table" and (not maxdepth or i < maxdepth) then + if not seen[v] then + seen[v] = true + dumptable(v, maxdepth, i+1, seen) + else + perror(string.rep("\t", i) .. "*** RECURSION ***") + end + end + end +end + + +-- +-- String and data manipulation routines +-- + +-- compatibility wrapper for xml.pcdata +function pcdata(value) + local xml = require "luci.xml" + + perror("luci.util.pcdata() has been replaced by luci.xml.pcdata() - Please update your code.") + return xml.pcdata(value) +end + +function urlencode(value) + if value ~= nil then + local str = tostring(value) + return lhttp.urlencode(str, lhttp.ENCODE_IF_NEEDED + lhttp.ENCODE_FULL) + or str + end + return nil +end + +function urldecode(value, decode_plus) + if value ~= nil then + local flag = decode_plus and lhttp.DECODE_PLUS or 0 + local str = tostring(value) + return lhttp.urldecode(str, lhttp.DECODE_IF_NEEDED + flag) + or str + end + return nil +end + +-- compatibility wrapper for xml.striptags +function striptags(value) + local xml = require "luci.xml" + + perror("luci.util.striptags() has been replaced by luci.xml.striptags() - Please update your code.") + return xml.striptags(value) +end + +function shellquote(value) + return string.format("'%s'", string.gsub(value or "", "'", "'\\''")) +end + +-- for bash, ash and similar shells single-quoted strings are taken +-- literally except for single quotes (which terminate the string) +-- (and the exception noted below for dash (-) at the start of a +-- command line parameter). +function shellsqescape(value) + local res + res, _ = string.gsub(value, "'", "'\\''") + return res +end + +-- bash, ash and other similar shells interpret a dash (-) at the start +-- of a command-line parameters as an option indicator regardless of +-- whether it is inside a single-quoted string. It must be backlash +-- escaped to resolve this. This requires in some funky special-case +-- handling. It may actually be a property of the getopt function +-- rather than the shell proper. +function shellstartsqescape(value) + res, _ = string.gsub(value, "^%-", "\\-") + return shellsqescape(res) +end + +-- containing the resulting substrings. The optional max parameter specifies +-- the number of bytes to process, regardless of the actual length of the given +-- string. The optional last parameter, regex, specifies whether the separator +-- sequence is interpreted as regular expression. +-- pattern as regular expression (optional, default is false) +function split(str, pat, max, regex) + pat = pat or "\n" + max = max or #str + + local t = {} + local c = 1 + + if #str == 0 then + return {""} + end + + if #pat == 0 then + return nil + end + + if max == 0 then + return str + end + + repeat + local s, e = str:find(pat, c, not regex) + max = max - 1 + if s and max < 0 then + t[#t+1] = str:sub(c) + else + t[#t+1] = str:sub(c, s and s - 1) + end + c = e and e + 1 or #str + 1 + until not s or max < 0 + + return t +end + +function trim(str) + return (str:gsub("^%s*(.-)%s*$", "%1")) +end + +function cmatch(str, pat) + local count = 0 + for _ in str:gmatch(pat) do count = count + 1 end + return count +end + +-- one token per invocation, the tokens are separated by whitespace. If the +-- input value is a table, it is transformed into a string first. A nil value +-- will result in a valid iterator which aborts with the first invocation. +function imatch(v) + if type(v) == "table" then + local k = nil + return function() + k = next(v, k) + return v[k] + end + + elseif type(v) == "number" or type(v) == "boolean" then + local x = true + return function() + if x then + x = false + return tostring(v) + end + end + + elseif type(v) == "userdata" or type(v) == "string" then + return tostring(v):gmatch("%S+") + end + + return function() end +end + +-- value or 0 if the unit is unknown. Upper- or lower case is irrelevant. +-- Recognized units are: +-- o "y" - one year (60*60*24*366) +-- o "m" - one month (60*60*24*31) +-- o "w" - one week (60*60*24*7) +-- o "d" - one day (60*60*24) +-- o "h" - one hour (60*60) +-- o "min" - one minute (60) +-- o "kb" - one kilobyte (1024) +-- o "mb" - one megabyte (1024*1024) +-- o "gb" - one gigabyte (1024*1024*1024) +-- o "kib" - one si kilobyte (1000) +-- o "mib" - one si megabyte (1000*1000) +-- o "gib" - one si gigabyte (1000*1000*1000) +function parse_units(ustr) + + local val = 0 + + -- unit map + local map = { + -- date stuff + y = 60 * 60 * 24 * 366, + m = 60 * 60 * 24 * 31, + w = 60 * 60 * 24 * 7, + d = 60 * 60 * 24, + h = 60 * 60, + min = 60, + + -- storage sizes + kb = 1024, + mb = 1024 * 1024, + gb = 1024 * 1024 * 1024, + + -- storage sizes (si) + kib = 1000, + mib = 1000 * 1000, + gib = 1000 * 1000 * 1000 + } + + -- parse input string + for spec in ustr:lower():gmatch("[0-9%.]+[a-zA-Z]*") do + + local num = spec:gsub("[^0-9%.]+$","") + local spn = spec:gsub("^[0-9%.]+", "") + + if map[spn] or map[spn:sub(1,1)] then + val = val + num * ( map[spn] or map[spn:sub(1,1)] ) + else + val = val + num + end + end + + + return val +end + +-- also register functions above in the central string class for convenience +string.split = split +string.trim = trim +string.cmatch = cmatch +string.parse_units = parse_units + + +function append(src, ...) + for i, a in ipairs({...}) do + if type(a) == "table" then + for j, v in ipairs(a) do + src[#src+1] = v + end + else + src[#src+1] = a + end + end + return src +end + +function combine(...) + return append({}, ...) +end + +function contains(table, value) + for k, v in pairs(table) do + if value == v then + return k + end + end + return false +end + +-- Both table are - in fact - merged together. +function update(t, updates) + for k, v in pairs(updates) do + t[k] = v + end +end + +function keys(t) + local keys = { } + if t then + for k, _ in kspairs(t) do + keys[#keys+1] = k + end + end + return keys +end + +function clone(object, deep) + local copy = {} + + for k, v in pairs(object) do + if deep and type(v) == "table" then + v = clone(v, deep) + end + copy[k] = v + end + + return setmetatable(copy, getmetatable(object)) +end + + +-- Serialize the contents of a table value. +function _serialize_table(t, seen) + assert(not seen[t], "Recursion detected.") + seen[t] = true + + local data = "" + local idata = "" + local ilen = 0 + + for k, v in pairs(t) do + if type(k) ~= "number" or k < 1 or math.floor(k) ~= k or ( k - #t ) > 3 then + k = serialize_data(k, seen) + v = serialize_data(v, seen) + data = data .. ( #data > 0 and ", " or "" ) .. + '[' .. k .. '] = ' .. v + elseif k > ilen then + ilen = k + end + end + + for i = 1, ilen do + local v = serialize_data(t[i], seen) + idata = idata .. ( #idata > 0 and ", " or "" ) .. v + end + + return idata .. ( #data > 0 and #idata > 0 and ", " or "" ) .. data +end + +-- with loadstring(). +function serialize_data(val, seen) + seen = seen or setmetatable({}, {__mode="k"}) + + if val == nil then + return "nil" + elseif type(val) == "number" then + return val + elseif type(val) == "string" then + return "%q" % val + elseif type(val) == "boolean" then + return val and "true" or "false" + elseif type(val) == "function" then + return "loadstring(%q)" % get_bytecode(val) + elseif type(val) == "table" then + return "{ " .. _serialize_table(val, seen) .. " }" + else + return '"[unhandled data type:' .. type(val) .. ']"' + end +end + +function restore_data(str) + return loadstring("return " .. str)() +end + + +-- +-- Byte code manipulation routines +-- + +-- will be stripped before it is returned. +function get_bytecode(val) + local code + + if type(val) == "function" then + code = string.dump(val) + else + code = string.dump( loadstring( "return " .. serialize_data(val) ) ) + end + + return code -- and strip_bytecode(code) +end + +-- numbers and debugging numbers will be discarded. Original version by +-- Peter Cawley (http://lua-users.org/lists/lua-l/2008-02/msg01158.html) +function strip_bytecode(code) + local version, format, endian, int, size, ins, num, lnum = code:byte(5, 12) + local subint + if endian == 1 then + subint = function(code, i, l) + local val = 0 + for n = l, 1, -1 do + val = val * 256 + code:byte(i + n - 1) + end + return val, i + l + end + else + subint = function(code, i, l) + local val = 0 + for n = 1, l, 1 do + val = val * 256 + code:byte(i + n - 1) + end + return val, i + l + end + end + + local function strip_function(code) + local count, offset = subint(code, 1, size) + local stripped = { string.rep("\0", size) } + local dirty = offset + count + offset = offset + count + int * 2 + 4 + offset = offset + int + subint(code, offset, int) * ins + count, offset = subint(code, offset, int) + for n = 1, count do + local t + t, offset = subint(code, offset, 1) + if t == 1 then + offset = offset + 1 + elseif t == 4 then + offset = offset + size + subint(code, offset, size) + elseif t == 3 then + offset = offset + num + elseif t == 254 or t == 9 then + offset = offset + lnum + end + end + count, offset = subint(code, offset, int) + stripped[#stripped+1] = code:sub(dirty, offset - 1) + for n = 1, count do + local proto, off = strip_function(code:sub(offset, -1)) + stripped[#stripped+1] = proto + offset = offset + off - 1 + end + offset = offset + subint(code, offset, int) * int + int + count, offset = subint(code, offset, int) + for n = 1, count do + offset = offset + subint(code, offset, size) + size + int * 2 + end + count, offset = subint(code, offset, int) + for n = 1, count do + offset = offset + subint(code, offset, size) + size + end + stripped[#stripped+1] = string.rep("\0", int * 3) + return table.concat(stripped), offset + end + + return code:sub(1,12) .. strip_function(code:sub(13,-1)) +end + + +-- +-- Sorting iterator functions +-- + +function _sortiter( t, f ) + local keys = { } + + local k, v + for k, v in pairs(t) do + keys[#keys+1] = k + end + + local _pos = 0 + + table.sort( keys, f ) + + return function() + _pos = _pos + 1 + if _pos <= #keys then + return keys[_pos], t[keys[_pos]], _pos + end + end +end + +-- the provided callback function. +function spairs(t,f) + return _sortiter( t, f ) +end + +-- The table pairs are sorted by key. +function kspairs(t) + return _sortiter( t ) +end + +-- The table pairs are sorted by value. +function vspairs(t) + return _sortiter( t, function (a,b) return t[a] < t[b] end ) +end + + +-- +-- System utility functions +-- + +function bigendian() + return string.byte(string.dump(function() end), 7) == 0 +end + +function exec(command) + local pp = io.popen(command) + local data = pp:read("*a") + pp:close() + + return data +end + +function execi(command) + local pp = io.popen(command) + + return pp and function() + local line = pp:read() + + if not line then + pp:close() + end + + return line + end +end + +-- Deprecated +function execl(command) + local pp = io.popen(command) + local line = "" + local data = {} + + while true do + line = pp:read() + if (line == nil) then break end + data[#data+1] = line + end + pp:close() + + return data +end + + +local ubus_codes = { + "INVALID_COMMAND", + "INVALID_ARGUMENT", + "METHOD_NOT_FOUND", + "NOT_FOUND", + "NO_DATA", + "PERMISSION_DENIED", + "TIMEOUT", + "NOT_SUPPORTED", + "UNKNOWN_ERROR", + "CONNECTION_FAILED" +} + +local function ubus_return(...) + if select('#', ...) == 2 then + local rv, err = select(1, ...), select(2, ...) + if rv == nil and type(err) == "number" then + return nil, err, ubus_codes[err] + end + end + + return ... +end + +function ubus(object, method, data, path, timeout) + if not _ubus_connection then + _ubus_connection = _ubus.connect(path, timeout) + assert(_ubus_connection, "Unable to establish ubus connection") + end + + if object and method then + if type(data) ~= "table" then + data = { } + end + return ubus_return(_ubus_connection:call(object, method, data)) + elseif object then + return _ubus_connection:signatures(object) + else + return _ubus_connection:objects() + end +end + +function serialize_json(x, cb) + local js = json.stringify(x) + if type(cb) == "function" then + cb(js) + else + return js + end +end + + +function libpath() + return require "nixio.fs".dirname(ldebug.__file__) +end + +function checklib(fullpathexe, wantedlib) + local fs = require "nixio.fs" + local haveldd = fs.access('/usr/bin/ldd') + local haveexe = fs.access(fullpathexe) + if not haveldd or not haveexe then + return false + end + local libs = exec(string.format("/usr/bin/ldd %s", shellquote(fullpathexe))) + if not libs then + return false + end + for k, v in ipairs(split(libs)) do + if v:find(wantedlib) then + return true + end + end + return false +end + +------------------------------------------------------------------------------- +-- Coroutine safe xpcall and pcall versions +-- +-- Encapsulates the protected calls with a coroutine based loop, so errors can +-- be dealed without the usual Lua 5.x pcall/xpcall issues with coroutines +-- yielding inside the call to pcall or xpcall. +-- +-- Authors: Roberto Ierusalimschy and Andre Carregal +-- Contributors: Thomas Harning Jr., Ignacio Burgueño, Fabio Mascarenhas +-- +-- Copyright 2005 - Kepler Project +-- +-- $Id: coxpcall.lua,v 1.13 2008/05/19 19:20:02 mascarenhas Exp $ +------------------------------------------------------------------------------- + +------------------------------------------------------------------------------- +-- Implements xpcall with coroutines +------------------------------------------------------------------------------- +local coromap = setmetatable({}, { __mode = "k" }) + +local function handleReturnValue(err, co, status, ...) + if not status then + return false, err(debug.traceback(co, (...)), ...) + end + if coroutine.status(co) == 'suspended' then + return performResume(err, co, coroutine.yield(...)) + else + return true, ... + end +end + +function performResume(err, co, ...) + return handleReturnValue(err, co, coroutine.resume(co, ...)) +end + +local function id(trace, ...) + return trace +end + +function coxpcall(f, err, ...) + local current = coroutine.running() + if not current then + if err == id then + return pcall(f, ...) + else + if select("#", ...) > 0 then + local oldf, params = f, { ... } + f = function() return oldf(unpack(params)) end + end + return xpcall(f, err) + end + else + local res, co = pcall(coroutine.create, f) + if not res then + local newf = function(...) return f(...) end + co = coroutine.create(newf) + end + coromap[co] = current + coxpt[co] = coxpt[current] or current or 0 + return performResume(err, co, ...) + end +end + +function copcall(f, ...) + return coxpcall(f, id, ...) +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/util.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/util.luac new file mode 100644 index 000000000000..6cabeaf2b205 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/util.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/version.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/version.lua new file mode 100644 index 000000000000..79a4e6788e49 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/version.lua @@ -0,0 +1,20 @@ +local pcall, dofile, _G = pcall, dofile, _G + +module "luci.version" + +if pcall(dofile, "/etc/openwrt_release") and _G.DISTRIB_DESCRIPTION then + distname = "" + distversion = _G.DISTRIB_DESCRIPTION + if _G.DISTRIB_REVISION then + distrevision = _G.DISTRIB_REVISION + if not distversion:find(distrevision,1,true) then + distversion = distversion .. " " .. distrevision + end + end +else + distname = "OpenWrt" + distversion = "Development Snapshot" +end + +luciname = "LuCI master-snmp branch" +luciversion = "git-23.052.04749-af5524f" diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/version.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/version.luac new file mode 100644 index 000000000000..01186d23b47b Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/version.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/xml.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/xml.lua new file mode 100644 index 000000000000..30b37210bd83 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/xml.lua @@ -0,0 +1,26 @@ +-- Copyright 2008 Steven Barth +-- Licensed to the public under the Apache License 2.0. + +local tparser = require "luci.template.parser" +local string = require "string" + +local tostring = tostring + +module "luci.xml" + +-- +-- String and data manipulation routines +-- + +function pcdata(value) + return value and tparser.pcdata(tostring(value)) +end + +function striptags(value) + return value and tparser.striptags(tostring(value)) +end + + +-- also register functions above in the central string class for convenience +string.pcdata = pcdata +string.striptags = striptags diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/xml.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/xml.luac new file mode 100644 index 000000000000..caa0e16f6234 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/luci/xml.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/mime.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/mime.lua new file mode 100644 index 000000000000..d3abac51cad5 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/mime.lua @@ -0,0 +1,89 @@ +----------------------------------------------------------------------------- +-- MIME support for the Lua language. +-- Author: Diego Nehab +-- Conforming to RFCs 2045-2049 +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local ltn12 = require("ltn12") +local mime = require("mime.core") +local string = require("string") +local _M = mime + +-- encode, decode and wrap algorithm tables +local encodet, decodet, wrapt = {},{},{} + +_M.encodet = encodet +_M.decodet = decodet +_M.wrapt = wrapt + +-- creates a function that chooses a filter by name from a given table +local function choose(table) + return function(name, opt1, opt2) + if base.type(name) ~= "string" then + name, opt1, opt2 = "default", name, opt1 + end + local f = table[name or "nil"] + if not f then + base.error("unknown key (" .. base.tostring(name) .. ")", 3) + else return f(opt1, opt2) end + end +end + +-- define the encoding filters +encodet['base64'] = function() + return ltn12.filter.cycle(_M.b64, "") +end + +encodet['quoted-printable'] = function(mode) + return ltn12.filter.cycle(_M.qp, "", + (mode == "binary") and "=0D=0A" or "\r\n") +end + +-- define the decoding filters +decodet['base64'] = function() + return ltn12.filter.cycle(_M.unb64, "") +end + +decodet['quoted-printable'] = function() + return ltn12.filter.cycle(_M.unqp, "") +end + +local function format(chunk) + if chunk then + if chunk == "" then return "''" + else return string.len(chunk) end + else return "nil" end +end + +-- define the line-wrap filters +wrapt['text'] = function(length) + length = length or 76 + return ltn12.filter.cycle(_M.wrp, length, length) +end +wrapt['base64'] = wrapt['text'] +wrapt['default'] = wrapt['text'] + +wrapt['quoted-printable'] = function() + return ltn12.filter.cycle(_M.qpwrp, 76, 76) +end + +-- function that choose the encoding, decoding or wrap algorithm +_M.encode = choose(encodet) +_M.decode = choose(decodet) +_M.wrap = choose(wrapt) + +-- define the end-of-line normalization filter +function _M.normalize(marker) + return ltn12.filter.cycle(_M.eol, 0, marker) +end + +-- high level stuffing filter +function _M.stuff() + return ltn12.filter.cycle(_M.dot, 2) +end + +return _M diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/mime.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/mime.luac new file mode 100644 index 000000000000..fa9a18267509 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/mime.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/mime/core.so b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/mime/core.so new file mode 120000 index 000000000000..f6709c024e89 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/mime/core.so @@ -0,0 +1 @@ +../mime-1.0.3.so \ No newline at end of file diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/mtkwifi.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/mtkwifi.lua new file mode 100755 index 000000000000..6c50b46f7dc3 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/mtkwifi.lua @@ -0,0 +1,2090 @@ +#!/usr/bin/env lua + +--[[ + * A lua library to manipulate mtk's wifi driver. used in luci-app-mtk. + * + * Copyright (C) 2016 Hua Shao + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +]] +require("datconf") +local ioctl_help = require "ioctl_helper" +local mtkwifi = {} +local logDisable = 1 +function debug_write(...) + -- luci.http.write(...) + if logDisable == 1 then + return + end + local syslog_msg = ""; + local ff = io.open("/tmp/mtkwifi", "a") + local nargs = select('#',...) + + + + for n=1, nargs do + local v = select(n,...) + if (type(v) == "string" or type(v) == "number") then + ff:write(v.." ") + syslog_msg = syslog_msg..v.." "; + elseif (type(v) == "boolean") then + if v then + ff:write("true ") + syslog_msg = syslog_msg.."true "; + else + ff:write("false ") + syslog_msg = syslog_msg.."false "; + end + elseif (type(v) == "nil") then + ff:write("nil ") + syslog_msg = syslog_msg.."nil "; + else + ff:write(" ") + syslog_msg = syslog_msg.." "; + end + end + ff:write("\n") + ff:close() + nixio.syslog("debug", syslog_msg) +end + +function mtkwifi.get_table_length(T) + local count = 0 + for _ in pairs(T) do + count = count + 1 + end + return count +end + +function mtkwifi.get_file_lines(fileName) + local fd = io.open(fileName, "r") + if not fd then return end + local content = fd:read("*all") + fd:close() + return mtkwifi.__lines(content) +end + +function mtkwifi.__split(s, delimiter) + if s == nil then s = "0" end + local result = {}; + for match in (s..delimiter):gmatch("(.-)"..delimiter) do + table.insert(result, match); + end + return result; +end + +function string:split(sep) + local sep, fields = sep or ":", {} + local pattern = string.format("([^%s]+)", sep) + self:gsub(pattern, function(c) fields[#fields+1] = c end) + return fields +end + +function mtkwifi.__trim(s) + if s then return (s:gsub("^%s*(.-)%s*$", "%1")) end +end + +function mtkwifi.__handleSpecialChars(s) + s = s:gsub("\\", "\\\\") + s = s:gsub("\"", "\\\"") + return s +end + +function mtkwifi.__spairs(t, order) + -- collect the keys + local keys = {} + for k in pairs(t) do keys[#keys+1] = k end + -- if order function given, sort by it by passing the table and keys a, b, + -- otherwise just sort the keys + --[[ + if order then + table.sort(keys, function(a,b) return order(t, a, b) end) + -- table.sort(keys, order) + else + table.sort(keys) + end + ]] + table.sort(keys, order) + -- return the iterator function + local i = 0 + return function() + i = i + 1 + if keys[i] then + return keys[i], t[keys[i]] + end + end +end + +function mtkwifi.__lines(str) + local t = {} + local function helper(line) table.insert(t, line) return "" end + helper((str:gsub("(.-)\r?\n", helper))) + return t +end + +function mtkwifi.__get_l1dat() + if not pcall(require, "l1dat_parser") then + return + end + + local parser = require("l1dat_parser") + local l1dat = parser.load_l1_profile(parser.L1_DAT_PATH) + + return l1dat, parser +end + +function mtkwifi.sleep(s) + local ntime = os.clock() + s + repeat until os.clock() > ntime +end + +function mtkwifi.deepcopy(orig) + local orig_type = type(orig) + local copy + if orig_type == 'table' then + copy = {} + for orig_key, orig_value in next, orig, nil do + copy[mtkwifi.deepcopy(orig_key)] = mtkwifi.deepcopy(orig_value) + end + setmetatable(copy, mtkwifi.deepcopy(getmetatable(orig))) + else -- number, string, boolean, etc + copy = orig + end + return copy +end + +function mtkwifi.read_pipe(pipe) + local retry_count = 10 + local fp, txt, err + repeat -- fp:read() may return error, "Interrupted system call", and can be recovered by doing it again + fp = io.popen(pipe) + txt, err = fp:read("*a") + fp:close() + retry_count = retry_count - 1 + until err == nil or retry_count == 0 + return txt +end + +function mtkwifi.detect_triband() + local devs = mtkwifi.get_all_devs() + local l1dat, l1 = mtkwifi.__get_l1dat() + local dridx = l1.DEV_RINDEX + local main_ifname + local bands = 0 + for _,dev in ipairs(devs) do + main_ifname = l1dat and l1dat[dridx][dev.devname].main_ifname or dbdc_prefix[mainidx][subidx].."0" + if mtkwifi.exists("/sys/class/net/"..main_ifname) then + bands = bands + 1 + end + end + return bands +end + +function mtkwifi.detect_first_card() + local devs = mtkwifi.get_all_devs() + local first_card_profile + + for i,dev in ipairs(devs) do + first_card_profile = dev.profile + if i == 1 then break end + end + + return first_card_profile +end + +function mtkwifi.load_profile(path, raw) + local cfgs = {} + + cfgobj = datconf.openfile(path) + if cfgobj then + cfgs = cfgobj:getall() + cfgobj:close() + elseif raw then + cfgs = datconf.parse(raw) + end + + return cfgs +end + +function mtkwifi.save_profile(cfgs, path) + + if not cfgs then + debug_write("configuration was empty, nothing saved") + return + end + + -- Keep a backup of last profile settings + -- if string.match(path, "([^/]+)\.dat") then + -- os.execute("cp -f "..path.." "..mtkwifi.__profile_previous_settings_path(path)) + -- end + local datobj = datconf.openfile(path) + datobj:merge(cfgs) + datobj:close(true) -- means close and commit + + if pcall(require, "mtknvram") then + local nvram = require("mtknvram") + local l1dat, l1 = mtkwifi.__get_l1dat() + local zone = l1 and l1.l1_path_to_zone(path) + + if pcall(require, "map_helper") and zone == "dev1" then + mtkwifi.save_easymesh_profile_to_nvram() + else + if not l1dat then + debug_write("save_profile: no l1dat", path) + nvram.nvram_save_profile(path) + else + if zone then + debug_write("save_profile:", path, zone) + nvram.nvram_save_profile(path, zone) + else + debug_write("save_profile:", path) + nvram.nvram_save_profile(path) + end + end + end + end + os.execute("sync >/dev/null 2>&1") +end + +function mtkwifi.split_profile(path, path_2g, path_5g) + assert(path) + assert(path_2g) + assert(path_5g) + local cfgs = mtkwifi.load_profile(path) + local dirty = { + "Channel", + "WirelessMode", + "TxRate", + "WmmCapable", + "NoForwarding", + "HideSSID", + "IEEE8021X", + "PreAuth", + "AuthMode", + "EncrypType", + "RekeyMethod", + "RekeyInterval", + "PMKCachePeriod", + "DefaultKeyId", + "Key{n}Type", + "HT_EXTCHA", + "RADIUS_Server", + "RADIUS_Port", + } + local cfg5g = mtkwifi.deepcopy(cfgs) + for _,v in ipairs(dirty) do + cfg5g[v] = mtkwifi.token_get(cfgs[v], 1, 0) + assert(cfg5g[v]) + end + mtkwifi.save_profile(cfg5g, path_5g) + + local cfg2g = mtkwifi.deepcopy(cfgs) + for _,v in ipairs(dirty) do + cfg2g[v] = mtkwifi.token_get(cfgs[v], 1, 0) + assert(cfg2g[v]) + end + mtkwifi.save_profile(cfg2g, path_2g) +end + +function mtkwifi.merge_profile(path, path_2g, path_5g) + local cfg2g = mtkwifi.load_profile(path_2g) + local cfg5g = mtkwifi.load_profile(path_5g) + local dirty = { + "Channel", + "WirelessMode", + "TxRate", + "WmmCapable", + "NoForwarding", + "HideSSID", + "IEEE8021X", + "PreAuth", + "AuthMode", + "EncrypType", + "RekeyMethod", + "RekeyInterval", + "PMKCachePeriod", + "DefaultKeyId", + "Key{n}Type", + "HT_EXTCHA", + "RADIUS_Server", + "RADIUS_Port", + } + local cfgs = mtkwifi.deepcopy(cfg2g) + for _,v in dirty do + -- TODO + end + mtkwifi.save_profile(cfgs, path) +end + +-- update path1 by path2 +function mtkwifi.update_profile(path1, path2) + local cfg1 = datconf.openfile(path1) + local cfg2 = datconf.openfile(path2) + + cfg1:merge(cfg2:getall()) + cfg1:close(true) + cfg2:close() + os.execute("sync >/dev/null 2>&1") +end + +function mtkwifi.__child_info_path() + local path = "/tmp/mtk/wifi/child_info.dat" + os.execute("mkdir -p /tmp/mtk/wifi") + return path +end + +function mtkwifi.__profile_previous_settings_path(profile) + assert(type(profile) == "string") + local bak = "/tmp/mtk/wifi/"..string.match(profile, "([^/]+)\.dat")..".last" + os.execute("mkdir -p /tmp/mtk/wifi") + return bak +end + +function mtkwifi.__profile_applied_settings_path(profile) + assert(type(profile) == "string") + local bak + if string.match(profile, "([^/]+)\.dat") then + os.execute("mkdir -p /tmp/mtk/wifi") + bak = "/tmp/mtk/wifi/"..string.match(profile, "([^/]+)\.dat")..".applied" + elseif string.match(profile, "([^/]+)\.txt") then + os.execute("mkdir -p /tmp/mtk/wifi") + bak = "/tmp/mtk/wifi/"..string.match(profile, "([^/]+)\.txt")..".applied" + elseif string.match(profile, "([^/]+)$") then + os.execute("mkdir -p /tmp/mtk/wifi") + bak = "/tmp/mtk/wifi/"..string.match(profile, "([^/]+)$")..".applied" + else + bak = "" + end + + return bak +end + +-- if path2 is not given, use backup of path1. +function mtkwifi.diff_profile(path1, path2) + assert(path1) + if not path2 then + path2 = mtkwifi.__profile_applied_settings_path(path1) + if not mtkwifi.exists(path2) then + return {} + end + end + assert(path2) + + local cfg1 + local cfg2 + local diff = {} + if path1 == mtkwifi.__easymesh_bss_cfgs_path() then + cfg1 = mtkwifi.get_file_lines(path1) or {} + cfg2 = mtkwifi.get_file_lines(path2) or {} + else + cfg1 = mtkwifi.load_profile(path1) or {} + cfg2 = mtkwifi.load_profile(path2) or {} + end + + for k,v in pairs(cfg1) do + if cfg2[k] ~= cfg1[k] then + diff[k] = {cfg1[k] or "", cfg2[k] or ""} + end + end + + for k,v in pairs(cfg2) do + if cfg2[k] ~= cfg1[k] then + diff[k] = {cfg1[k] or "", cfg2[k] or ""} + end + end + + return diff +end + +function mtkwifi.__fork_exec(command) + if type(command) ~= type("") or command == "" then + debug_write("__fork_exec : Incorrect command! Expected non-empty string type, got ",type(command)) + nixio.syslog("err", "__fork_exec : Incorrect command! Expected non-empty string type, got "..type(command)) + else + local nixio = require("nixio") + -- If nixio.exec() fails, then child process will be reaped automatically and + -- it will be achieved by ignoring SIGCHLD signal here in parent process! + if not nixio.signal(17,"ign") then + nixio.syslog("warning", "__fork_exec : Failed to set SIG_IGN for SIGCHLD!") + debug_write("__fork_exec : Failed to set SIG_IGN for SIGCHLD!") + end + local pid = nixio.fork() + if pid < 0 then + nixio.syslog("err", "__fork_exec : [Fork Failure] "..command) + debug_write("__fork_exec : [Fork Failure] "..command) + elseif pid == 0 then + -- change to root dir to flush out any opened directory streams of parent process. + nixio.chdir("/") + + -- As file descriptors are inherited by child process, all unused file descriptors must be closed. + -- Make stdin, out, err file descriptors point to /dev/null using dup2. + -- As a result, it will not corrupt stdin, out, err file descriptors of parent process. + local null = nixio.open("/dev/null", "w+") + if null then + nixio.dup(null, nixio.stderr) + nixio.dup(null, nixio.stdout) + nixio.dup(null, nixio.stdin) + if null:fileno() > 2 then + null:close() + end + end + debug_write("__fork_exec : cmd = "..command) + -- replaces the child process image with the new process image generated by provided command + nixio.exec("/bin/sh", "-c", command) + os.exit(true) + end + end +end + +function mtkwifi.is_child_active() + local fd = io.open(mtkwifi.__child_info_path(), "r") + if not fd then + os.execute("rm -f "..mtkwifi.__child_info_path()) + return false + end + local content = fd:read("*all") + fd:close() + if not content then + os.execute("rm -f "..mtkwifi.__child_info_path()) + return false + end + local active_pid_list = {} + for _,pid in ipairs(mtkwifi.__lines(content)) do + pid = pid:match("CHILD_PID=%s*(%d+)%s*") + if pid then + if tonumber(mtkwifi.read_pipe("ps | grep -v grep | grep -cw "..pid)) == 1 then + table.insert(active_pid_list, pid) + end + end + end + if next(active_pid_list) ~= nil then + return true + else + os.execute("rm -f "..mtkwifi.__child_info_path()) + return false + end + os.execute("sync >/dev/null 2>&1") +end + +function mtkwifi.__run_in_child_env(cbFn,...) + if type(cbFn) ~= "function" then + debug_write("__run_in_child_env : Function type expected, got ", type(cbFn)) + nixio.syslog("err", "__run_in_child_env : Function type expected, got "..type(cbFn)) + else + local unpack = unpack or table.unpack + local cbArgs = {...} + local nixio = require("nixio") + -- Let child process reap automatically! + if not nixio.signal(17,"ign") then + nixio.syslog("warning", "__run_in_child_env : Failed to set SIG_IGN for SIGCHLD!") + debug_write("__run_in_child_env : Failed to set SIG_IGN for SIGCHLD!") + end + local pid = nixio.fork() + if pid < 0 then + debug_write("__run_in_child_env : Fork failure") + nixio.syslog("err", "__run_in_child_env : Fork failure") + elseif pid == 0 then + -- Change to root dir to flush out any opened directory streams of parent process. + nixio.chdir("/") + + -- As file descriptors are inherited by child process, all unnecessary file descriptors must be closed. + -- Make stdin, out, err file descriptors point to /dev/null using dup2. + -- As a result, it will not corrupt stdin, out, err file descriptors of parent process. + local null = nixio.open("/dev/null", "w+") + if null then + nixio.dup(null, nixio.stderr) + nixio.dup(null, nixio.stdout) + nixio.dup(null, nixio.stdin) + if null:fileno() > 2 then + null:close() + end + end + local fd = io.open(mtkwifi.__child_info_path(), "a") + if fd then + fd:write("CHILD_PID=",nixio.getpid(),"\n") + fd:close() + end + cbFn(unpack(cbArgs)) + os.exit(true) + end + end + os.execute("sync >/dev/null 2>&1") +end + +-- Mode 12 and 13 are only available for STAs. +local WirelessModeList = { + [0] = "B/G mixed", + [1] = "B only", + [2] = "A only", + -- [3] = "A/B/G mixed", + [4] = "G only", + -- [5] = "A/B/G/GN/AN mixed", + [6] = "N in 2.4G only", + [7] = "G/GN", -- i.e., no CCK mode + [8] = "A/N in 5 band", + [9] = "B/G/GN mode", + -- [10] = "A/AN/G/GN mode", --not support B mode + [11] = "only N in 5G band", + -- [12] = "B/G/GN/A/AN/AC mixed", + -- [13] = "G/GN/A/AN/AC mixed", -- no B mode + [14] = "A/AC/AN mixed", + [15] = "AC/AN mixed", --but no A mode + [16] = "HE_2G mode", --HE Wireless Mode + [17] = "HE_5G mode", --HE Wireless Mode + [18] = "HE_6G mode", --HE Wireless Mode +} + +local DevicePropertyMap = { + -- 2.4G + { + device="MT7622", + band={"0", "1", "4", "9"}, + isPowerBoostSupported=true, + isMultiAPSupported=true, + isWPA3_192bitSupported=true + }, + + { + device="MT7620", + band={"0", "1", "4", "9"}, + maxTxStream=2, + maxRxStream=2, + maxVif=8 + }, + + { + device="MT7628", + band={"0", "1", "4", "6", "7", "9"}, + maxTxStream=2, + maxRxStream=2, + maxVif=8, + isMultiAPSupported=true, + isWPA3_192bitSupported=true + }, + + { + device="MT7603", + band={"0", "1", "4", "6", "7", "9"}, + maxTxStream=2, + maxRxStream=2, + maxVif=8, + isMultiAPSupported=true, + isWPA3_192bitSupported=true + }, + + -- 5G + { + device="MT7612", + band={"2", "8", "11", "14", "15"}, + maxTxStream=2, + maxRxStream=2, + }, + + { + device="MT7662", + band={"2", "8", "11", "14", "15"}, + maxTxStream=2, + maxRxStream=2, + }, + + -- Mix + { + device="MT7615", + band={"0", "1", "4", "9", "2", "8", "14", "15"}, + isPowerBoostSupported=false, + isMultiAPSupported=true, + isWPA3_192bitSupported=true, + maxVif=16, + maxDBDCVif=8 + }, + + { + device="MT7915", + band={"0", "1", "4", "9", "2", "8", "14", "15", "16", "17", "18"}, + isPowerBoostSupported=false, + isMultiAPSupported=true, + isWPA3_192bitSupported=true, + maxVif=16, + maxDBDCVif=16, + invalidChBwList={161} + }, + + { + device="MT7916", + band={"0", "1", "4", "9", "2", "8", "14", "15", "16", "17", "18"}, + isPowerBoostSupported=false, + isMultiAPSupported=true, + isWPA3_192bitSupported=true, + maxVif=16, + maxDBDCVif=16, + invalidChBwList={161}, + maxTxStream=2, + maxRxStream=2, + }, + + { + device="MT7981", + band={"0", "1", "4", "9", "2", "8", "14", "15", "16", "17", "18"}, + isPowerBoostSupported=false, + isMultiAPSupported=true, + isWPA3_192bitSupported=true, + maxVif=16, + maxDBDCVif=16, + invalidChBwList={161}, + maxTxStream=2, + maxRxStream=2, + }, + + { + device="MT7986", + band={"0", "1", "4", "9", "2", "8", "14", "15", "16", "17", "18"}, + isPowerBoostSupported=false, + isMultiAPSupported=true, + isWPA3_192bitSupported=true, + maxVif=16, + maxDBDCVif=16, + invalidChBwList={161}, + maxTxStream=4, + maxRxStream=4, + }, + + { + device="MT7663", + band={"0", "1", "4", "9", "2", "8", "14", "15"}, + maxTxStream=2, + maxRxStream=2, + invalidChBwList={160,161}, + isMultiAPSupported=true, + isWPA3_192bitSupported=true + }, + + { + device="MT7613", + band={"0", "1", "4", "9", "2", "8", "14", "15"}, + maxTxStream=2, + maxRxStream=2, + invalidChBwList={160,161}, + isMultiAPSupported=true, + isWPA3_192bitSupported=true + }, + + { + device="MT7626", + band={"0", "1", "4", "9", "2", "8", "14", "15"}, + maxTxStream=3, + maxRxStream=3, + invalidChBwList={160,161}, + wdsBand="2.4G", + mimoBand="5G", + maxDBDCVif=8 + }, + + { + device="MT7629", + band={"0", "1", "4", "9", "2", "8", "14", "15"}, + maxTxStream=3, + maxRxStream=3, + invalidChBwList={160,161}, + wdsBand="2.4G", + mimoBand="5G", + maxDBDCVif=8, + isMultiAPSupported=true + } +} + +mtkwifi.CountryRegionList_6G_All = { + {region=0, text="0: Ch1~233"}, + {region=1, text="1: Ch1~97"}, + {region=2, text="2: Ch101~117"}, + {region=3, text="3: Ch121~185"}, + {region=4, text="4: Ch189~233"}, + {region=5, text="5: Ch1~97"}, + {region=6, text="6: Ch1~97"}, + {region=7, text="7: Ch1~97, Ch101~109"}, +} + +mtkwifi.CountryRegionList_5G_All = { + {region=0, text="0: Ch36~64, Ch149~165"}, + {region=1, text="1: Ch36~64, Ch100~140"}, + {region=2, text="2: Ch36~64"}, + {region=3, text="3: Ch52~64, Ch149~161"}, + {region=4, text="4: Ch149~165"}, + {region=5, text="5: Ch149~161"}, + {region=6, text="6: Ch36~48"}, + {region=7, text="7: Ch36~64, Ch100~140, Ch149~165"}, + {region=8, text="8: Ch52~64"}, + {region=9, text="9: Ch36~64, Ch100~116, Ch132~140, Ch149~165"}, + {region=10, text="10: Ch36~48, Ch149~165"}, + {region=11, text="11: Ch36~64, Ch100~120, Ch149~161"}, + {region=12, text="12: Ch36~64, Ch100~144"}, + {region=13, text="13: Ch36~64, Ch100~144, Ch149~165"}, + {region=14, text="14: Ch36~64, Ch100~116, Ch132~144, Ch149~165"}, + {region=15, text="15: Ch149~173"}, + {region=16, text="16: Ch52~64, Ch149~165"}, + {region=17, text="17: Ch36~48, Ch149~161"}, + {region=18, text="18: Ch36~64, Ch100~116, Ch132~140"}, + {region=19, text="19: Ch56~64, Ch100~140, Ch149~161"}, + {region=20, text="20: Ch36~64, Ch100~124, Ch149~161"}, + {region=21, text="21: Ch36~64, Ch100~140, Ch149~161"}, + {region=22, text="22: Ch100~140"}, + {region=30, text="30: Ch36~48, Ch52~64, Ch100~140, Ch149~165"}, + {region=31, text="31: Ch52~64, Ch100~140, Ch149~165"}, + {region=32, text="32: Ch36~48, Ch52~64, Ch100~140, Ch149~161"}, + {region=33, text="33: Ch36~48, Ch52~64, Ch100~140"}, + {region=34, text="34: Ch36~48, Ch52~64, Ch149~165"}, + {region=35, text="35: Ch36~48, Ch52~64"}, + {region=36, text="36: Ch36~48, Ch100~140, Ch149~165"}, + {region=37, text="37: Ch36~48, Ch52~64, Ch149~165, Ch173"} +} + +mtkwifi.CountryRegionList_2G_All = { + {region=0, text="0: Ch1~11"}, + {region=1, text="1: Ch1~13"}, + {region=2, text="2: Ch10~11"}, + {region=3, text="3: Ch10~13"}, + {region=4, text="4: Ch14"}, + {region=5, text="5: Ch1~14"}, + {region=6, text="6: Ch3~9"}, + {region=7, text="7: Ch5~13"}, + {region=31, text="31: Ch1~11, Ch12~14"}, + {region=32, text="32: Ch1~11, Ch12~13"}, + {region=33, text="33: Ch1~14"} +} + +mtkwifi.ChannelList_6G_All = { + {channel= 0 , text="Channel 0 (Auto )", region={}}, + {channel= 1 , text="Channel 1 (5.955 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 5 , text="Channel 5 (5.975 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 9 , text="Channel 9 (5.995 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 13 , text="Channel 13 (6.015 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 17 , text="Channel 17 (6.035 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 21 , text="Channel 21 (6.055 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 25 , text="Channel 25 (6.075 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 29 , text="Channel 29 (6.095 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 33 , text="Channel 33 (6.115 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 37 , text="Channel 37 (6.135 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 41 , text="Channel 41 (6.155 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 45 , text="Channel 45 (6.175 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 49 , text="Channel 49 (6.195 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 53 , text="Channel 53 (6.215 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 57 , text="Channel 57 (6.235 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 61 , text="Channel 61 (6.255 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 65 , text="Channel 65 (6.275 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 69 , text="Channel 69 (6.295 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 73 , text="Channel 73 (6.315 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 77 , text="Channel 77 (6.335 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 81 , text="Channel 81 (6.355 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 85 , text="Channel 85 (6.375 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 89 , text="Channel 89 (6.395 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 93 , text="Channel 93 (6.415 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 97 , text="Channel 97 (6.435 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1}}, + {channel= 101, text="Channel 101 (6.455 GHz)", region={[0]=1, [2]=1, [7]=1}}, + {channel= 105, text="Channel 105 (6.475 GHz)", region={[0]=1, [2]=1, [7]=1}}, + {channel= 109, text="Channel 109 (6.495 GHz)", region={[0]=1, [2]=1, [7]=1}}, + {channel= 113, text="Channel 113 (6.515 GHz)", region={[0]=1, [2]=1}}, + {channel= 117, text="Channel 117 (6.535 GHz)", region={[0]=1, [2]=1}}, + {channel= 121, text="Channel 121 (6.555 GHz)", region={[0]=1, [3]=1}}, + {channel= 125, text="Channel 125 (6.575 GHz)", region={[0]=1, [3]=1}}, + {channel= 129, text="Channel 129 (6.595 GHz)", region={[0]=1, [3]=1}}, + {channel= 133, text="Channel 133 (6.615 GHz)", region={[0]=1, [3]=1}}, + {channel= 137, text="Channel 137 (6.635 GHz)", region={[0]=1, [3]=1}}, + {channel= 141, text="Channel 141 (6.655 GHz)", region={[0]=1, [3]=1}}, + {channel= 145, text="Channel 145 (6.675 GHz)", region={[0]=1, [3]=1}}, + {channel= 149, text="Channel 149 (6.695 GHz)", region={[0]=1, [3]=1}}, + {channel= 153, text="Channel 153 (6.715 GHz)", region={[0]=1, [3]=1}}, + {channel= 157, text="Channel 157 (6.735 GHz)", region={[0]=1, [3]=1}}, + {channel= 161, text="Channel 161 (6.755 GHz)", region={[0]=1, [3]=1}}, + {channel= 165, text="Channel 165 (6.775 GHz)", region={[0]=1, [3]=1}}, + {channel= 169, text="Channel 169 (6.795 GHz)", region={[0]=1, [3]=1}}, + {channel= 173, text="Channel 173 (6.815 GHz)", region={[0]=1, [3]=1}}, + {channel= 177, text="Channel 177 (6.835 GHz)", region={[0]=1, [3]=1}}, + {channel= 181, text="Channel 181 (6.855 GHz)", region={[0]=1, [3]=1}}, + {channel= 185, text="Channel 185 (6.875 GHz)", region={[0]=1, [3]=1}}, + {channel= 189, text="Channel 189 (6.895 GHz)", region={[0]=1, [4]=1}}, + {channel= 193, text="Channel 193 (6.915 GHz)", region={[0]=1, [4]=1}}, + {channel= 197, text="Channel 197 (6.935 GHz)", region={[0]=1, [4]=1}}, + {channel= 201, text="Channel 201 (6.955 GHz)", region={[0]=1, [4]=1}}, + {channel= 205, text="Channel 205 (6.975 GHz)", region={[0]=1, [4]=1}}, + {channel= 209, text="Channel 209 (6.995 GHz)", region={[0]=1, [4]=1}}, + {channel= 213, text="Channel 213 (7.015 GHz)", region={[0]=1, [4]=1}}, + {channel= 217, text="Channel 217 (7.035 GHz)", region={[0]=1, [4]=1}}, + {channel= 221, text="Channel 221 (7.055 GHz)", region={[0]=1, [4]=1}}, + {channel= 225, text="Channel 225 (7.075 GHz)", region={[0]=1, [4]=1}}, + {channel= 229, text="Channel 229 (7.095 GHz)", region={[0]=1, [4]=1}}, + {channel= 233, text="Channel 233 (7.115 GHz)", region={[0]=1, [4]=1}}, +} + +mtkwifi.ChannelList_5G_All = { + {channel=0, text="Channel 0 (Auto )", region={}}, + {channel= 36, text="Channel 36 (5.180 GHz)", region={[0]=1, [1]=1, [2]=1, [6]=1, [7]=1, [9]=1, [10]=1, [11]=1, [12]=1, [13]=1, [14]=1, [17]=1, [18]=1, [20]=1, [21]=1, [30]=1, [32]=1, [33]=1, [34]=1, [35]=1, [36]=1, [37]=1}}, + {channel= 40, text="Channel 40 (5.200 GHz)", region={[0]=1, [1]=1, [2]=1, [6]=1, [7]=1, [9]=1, [10]=1, [11]=1, [12]=1, [13]=1, [14]=1, [17]=1, [18]=1, [20]=1, [21]=1, [30]=1, [32]=1, [33]=1, [34]=1, [35]=1, [36]=1, [37]=1}}, + {channel= 44, text="Channel 44 (5.220 GHz)", region={[0]=1, [1]=1, [2]=1, [6]=1, [7]=1, [9]=1, [10]=1, [11]=1, [12]=1, [13]=1, [14]=1, [17]=1, [18]=1, [20]=1, [21]=1, [30]=1, [32]=1, [33]=1, [34]=1, [35]=1, [36]=1, [37]=1}}, + {channel= 48, text="Channel 48 (5.240 GHz)", region={[0]=1, [1]=1, [2]=1, [6]=1, [7]=1, [9]=1, [10]=1, [11]=1, [12]=1, [13]=1, [14]=1, [17]=1, [18]=1, [20]=1, [21]=1, [30]=1, [32]=1, [33]=1, [34]=1, [35]=1, [36]=1, [37]=1}}, + {channel= 52, text="Channel 52 (5.260 GHz)", region={[0]=1, [1]=1, [2]=1, [3]=1, [7]=1, [8]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [16]=1, [18]=1, [20]=1, [21]=1, [30]=1, [31]=1, [32]=1, [33]=1, [34]=1, [35]=1, [37]=1}}, + {channel= 56, text="Channel 56 (5.280 GHz)", region={[0]=1, [1]=1, [2]=1, [3]=1, [7]=1, [8]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [16]=1, [18]=1, [19]=1, [20]=1, [21]=1, [30]=1, [31]=1, [32]=1, [33]=1, [34]=1, [35]=1, [37]=1}}, + {channel= 60, text="Channel 60 (5.300 GHz)", region={[0]=1, [1]=1, [2]=1, [3]=1, [7]=1, [8]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [16]=1, [18]=1, [19]=1, [20]=1, [21]=1, [30]=1, [31]=1, [32]=1, [33]=1, [34]=1, [35]=1, [37]=1}}, + {channel= 64, text="Channel 64 (5.320 GHz)", region={[0]=1, [1]=1, [2]=1, [3]=1, [7]=1, [8]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [16]=1, [18]=1, [19]=1, [20]=1, [21]=1, [30]=1, [31]=1, [32]=1, [33]=1, [34]=1, [35]=1, [37]=1}}, + {channel=100, text="Channel 100 (5.500 GHz)", region={[1]=1, [7]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [18]=1, [19]=1, [20]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=104, text="Channel 104 (5.520 GHz)", region={[1]=1, [7]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [18]=1, [19]=1, [20]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=108, text="Channel 108 (5.540 GHz)", region={[1]=1, [7]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [18]=1, [19]=1, [20]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=112, text="Channel 112 (5.560 GHz)", region={[1]=1, [7]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [18]=1, [19]=1, [20]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=116, text="Channel 116 (5.580 GHz)", region={[1]=1, [7]=1, [9]=1, [11]=1, [12]=1, [13]=1, [14]=1, [18]=1, [19]=1, [20]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=120, text="Channel 120 (5.600 GHz)", region={[1]=1, [7]=1, [11]=1, [12]=1, [13]=1, [19]=1, [20]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=124, text="Channel 124 (5.620 GHz)", region={[1]=1, [7]=1, [12]=1, [13]=1, [19]=1, [20]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=128, text="Channel 128 (5.640 GHz)", region={[1]=1, [7]=1, [12]=1, [13]=1, [19]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=132, text="Channel 132 (5.660 GHz)", region={[1]=1, [7]=1, [9]=1, [12]=1, [13]=1, [14]=1, [18]=1, [19]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=136, text="Channel 136 (5.680 GHz)", region={[1]=1, [7]=1, [9]=1, [12]=1, [13]=1, [14]=1, [18]=1, [19]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=140, text="Channel 140 (5.700 GHz)", region={[1]=1, [7]=1, [9]=1, [12]=1, [13]=1, [14]=1, [18]=1, [19]=1, [21]=1, [22]=1, [30]=1, [31]=1, [32]=1, [33]=1, [36]=1}}, + {channel=144, text="Channel 144 (5.720 GHz)", region={[12]=1, [13]=1, [14]=1}}, + {channel=149, text="Channel 149 (5.745 GHz)", region={[0]=1, [3]=1, [4]=1, [5]=1, [7]=1, [9]=1, [10]=1, [11]=1, [13]=1, [14]=1, [15]=1, [16]=1, [17]=1, [19]=1, [20]=1, [21]=1, [30]=1, [31]=1, [32]=1, [34]=1, [36]=1, [37]=1}}, + {channel=153, text="Channel 153 (5.765 GHz)", region={[0]=1, [3]=1, [4]=1, [5]=1, [7]=1, [9]=1, [10]=1, [11]=1, [13]=1, [14]=1, [15]=1, [16]=1, [17]=1, [19]=1, [20]=1, [21]=1, [30]=1, [31]=1, [32]=1, [34]=1, [36]=1, [37]=1}}, + {channel=157, text="Channel 157 (5.785 GHz)", region={[0]=1, [3]=1, [4]=1, [5]=1, [7]=1, [9]=1, [10]=1, [11]=1, [13]=1, [14]=1, [15]=1, [16]=1, [17]=1, [19]=1, [20]=1, [21]=1, [30]=1, [31]=1, [32]=1, [34]=1, [36]=1, [37]=1}}, + {channel=161, text="Channel 161 (5.805 GHz)", region={[0]=1, [3]=1, [4]=1, [5]=1, [7]=1, [9]=1, [10]=1, [11]=1, [13]=1, [14]=1, [15]=1, [16]=1, [17]=1, [19]=1, [20]=1, [21]=1, [30]=1, [31]=1, [32]=1, [34]=1, [36]=1, [37]=1}}, + {channel=165, text="Channel 165 (5.825 GHz)", region={[0]=1, [4]=1, [7]=1, [9]=1, [10]=1, [13]=1, [14]=1, [15]=1, [16]=1, [30]=1, [31]=1, [34]=1, [36]=1, [37]=1}}, + {channel=169, text="Channel 169 (5.845 GHz)", region={[15]=1}}, + {channel=173, text="Channel 173 (5.865 GHz)", region={[15]=1, [37]=1}} +} + +mtkwifi.ChannelList_2G_All = { + {channel=0, text="Channel 0 (Auto )", region={}}, + {channel= 1, text="Channel 1 (2412 GHz)", region={[0]=1, [1]=1, [5]=1, [31]=1, [32]=1, [33]=1}}, + {channel= 2, text="Channel 2 (2417 GHz)", region={[0]=1, [1]=1, [5]=1, [31]=1, [32]=1, [33]=1}}, + {channel= 3, text="Channel 3 (2422 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [31]=1, [32]=1, [33]=1}}, + {channel= 4, text="Channel 4 (2427 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [31]=1, [32]=1, [33]=1}}, + {channel= 5, text="Channel 5 (2432 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel= 6, text="Channel 6 (2437 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel= 7, text="Channel 7 (2442 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel= 8, text="Channel 8 (2447 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel= 9, text="Channel 9 (2452 GHz)", region={[0]=1, [1]=1, [5]=1, [6]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel=10, text="Channel 10 (2457 GHz)", region={[0]=1, [1]=1, [2]=1, [3]=1, [5]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel=11, text="Channel 11 (2462 GHz)", region={[0]=1, [1]=1, [2]=1, [3]=1, [5]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel=12, text="Channel 12 (2467 GHz)", region={[1]=1, [3]=1, [5]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel=13, text="Channel 13 (2472 GHz)", region={[1]=1, [3]=1, [5]=1, [7]=1, [31]=1, [32]=1, [33]=1}}, + {channel=14, text="Channel 14 (2477 GHz)", region={[4]=1, [5]=1, [31]=1, [33]=1}} +} + +mtkwifi.ChannelList_5G_2nd_80MHZ_ALL = { + {channel=36, text="Ch36(5.180 GHz) - Ch48(5.240 GHz)", chidx=2}, + {channel=52, text="Ch52(5.260 GHz) - Ch64(5.320 GHz)", chidx=6}, + {channel=-1, text="Channel between 64 100", chidx=-1}, + {channel=100, text="Ch100(5.500 GHz) - Ch112(5.560 GHz)", chidx=10}, + {channel=112, text="Ch116(5.580 GHz) - Ch128(5.640 GHz)", chidx=14}, + {channel=-1, text="Channel between 128 132", chidx=-1}, + {channel=132, text="Ch132(5.660 GHz) - Ch144(5.720 GHz)", chidx=18}, + {channel=-1, text="Channel between 144 149", chidx=-1}, + {channel=149, text="Ch149(5.745 GHz) - Ch161(5.805 GHz)", chidx=22} +} + +local AuthModeList = { + "Disable", + "OPEN",--OPENWEP + "Enhanced Open", + "SHARED",--SHAREDWEP + "WEPAUTO", + "WPA2", + "WPA3", + "WPA3-192-bit", + "WPA2PSK", + "WPA3PSK", + "WPAPSKWPA2PSK", + "WPA2PSKWPA3PSK", + "WPA1WPA2", + "IEEE8021X" +} + +local AuthModeList_6G = { + "Enhanced Open", + "WPA3PSK" +} + +local WpsEnableAuthModeList = { + "Disable", + "OPEN",--OPENWEP + "WPA2PSK", + "WPAPSKWPA2PSK" +} + +local WpsEnableAuthModeList_6G = { + "Disable", + "WPA2PSK", + "WPAPSKWPA2PSK" +} + +local ApCliAuthModeList = { + "Disable", + "OPEN", + "SHARED", + "Enhanced Open", + "WPAPSK", + "WPA2PSK", + "WPA3PSK", + -- "WPAPSKWPA2PSK", + -- "WPA2PSKWPA3PSK", + -- "WPA", + -- "WPA2", + -- "WPAWPA2", + -- "8021X", +} + +local EncryptionTypeList = { + "WEP", + "TKIP", + "TKIPAES", + "AES", + "GCMP256" +} + +local EncryptionTypeList_6G = { + "WEP", + "AES", + "GCMP256" +} + +local dbdc_prefix = { + {"ra", "rax"}, + {"rai", "ray"}, + {"rae", "raz"} +} + +local dbdc_apcli_prefix = { + {"apcli", "apclix"}, + {"apclii", "apcliy"}, + {"apclie", "apcliz"} +} + +function mtkwifi.band(mode) + local i = tonumber(mode) + if i == 0 + or i == 1 + or i == 4 + or i == 6 + or i == 7 + or i == 9 + or i == 16 then + return "2.4G" + elseif i == 18 then + return "6G" + else + return "5G" + end +end + + +function mtkwifi.__cfg2list(str) + -- delimeter == ";" + local i = 1 + local list = {} + for k in string.gmatch(str, "([^;]+)") do + list[i] = k + i = i + 1 + end + return list +end + +function mtkwifi.token_set(str, n, v) + -- n start from 1 + -- delimeter == ";" + if not str then return end + local tmp = mtkwifi.__cfg2list(str) + if type(v) ~= type("") and type(v) ~= type(0) then + nixio.syslog("err", "invalid value type in token_set, "..type(v)) + return + end + if #tmp < tonumber(n) then + for i=#tmp, tonumber(n) do + if not tmp[i] then + tmp[i] = v -- pad holes with v ! + end + end + else + tmp[n] = v + end + return table.concat(tmp, ";"):gsub("^;*(.-);*$", "%1"):gsub(";+",";") +end + + +function mtkwifi.token_get(str, n, v) + -- n starts from 1 + -- v is the backup in case token n is nil + if not str then return v end + local tmp = mtkwifi.__cfg2list(str) + return tmp[tonumber(n)] or v +end + +function mtkwifi.search_dev_and_profile_orig() + local nixio = require("nixio") + local dir = io.popen("ls /etc/wireless/") + if not dir then return end + local result = {} + -- case 1: mt76xx.dat (best) + -- case 2: mt76xx.n.dat (multiple card of same dev) + -- case 3: mt76xx.n.nG.dat (case 2 plus dbdc and multi-profile, bloody hell....) + for line in dir:lines() do + -- nixio.syslog("debug", "scan "..line) + local tmp = io.popen("find /etc/wireless/"..line.." -type f -name \"*.dat\"") + for datfile in tmp:lines() do + -- nixio.syslog("debug", "test "..datfile) + + repeat do + -- for case 1 + local devname = string.match(datfile, "("..line..").dat") + if devname then + result[devname] = datfile + -- nixio.syslog("debug", "yes "..devname.."="..datfile) + break + end + -- for case 2 + local devname = string.match(datfile, "("..line.."%.%d)%.dat") + if devname then + result[devname] = datfile + -- nixio.syslog("debug", "yes "..devname.."="..datfile) + break + end + -- for case 3 + local devname = string.match(datfile, "("..line.."%.%d%.%dG)%.dat") + if devname then + result[devname] = datfile + -- nixio.syslog("debug", "yes "..devname.."="..datfile) + break + end + end until true + end + end + + for k,v in pairs(result) do + nixio.syslog("debug", "search_dev_and_profile_orig: "..k.."="..v) + end + + return result +end + +function mtkwifi.search_dev_and_profile_l1() + local l1dat = mtkwifi.__get_l1dat() + + if not l1dat then return end + + local nixio = require("nixio") + local result = {} + local dbdc_2nd_if = "" + + for k, dev in ipairs(l1dat) do + dbdc_2nd_if = mtkwifi.token_get(dev.main_ifname, 2, nil) + if dbdc_2nd_if then + result[dev["INDEX"].."."..dev["mainidx"]..".1"] = mtkwifi.token_get(dev.profile_path, 1, nil) + result[dev["INDEX"].."."..dev["mainidx"]..".2"] = mtkwifi.token_get(dev.profile_path, 2, nil) + else + result[dev["INDEX"].."."..dev["mainidx"]] = dev.profile_path + end + end + + for k,v in pairs(result) do + nixio.syslog("debug", "search_dev_and_profile_l1: "..k.."="..v) + end + + return result +end + +function mtkwifi.search_dev_and_profile() + return mtkwifi.search_dev_and_profile_l1() or mtkwifi.search_dev_and_profile_orig() +end + +function mtkwifi.__setup_vifs(cfgs, devname, mainidx, subidx) + local l1dat, l1 = mtkwifi.__get_l1dat() + local dridx = l1dat and l1.DEV_RINDEX + + local prefix + local main_ifname + local vifs = {} + local dev_idx = "" + + + prefix = l1dat and l1dat[dridx][devname].ext_ifname or dbdc_prefix[mainidx][subidx] + + dev_idx = string.match(devname, "(%w+)") + + vifs["__prefix"] = prefix + if (cfgs.BssidNum == nil) then + debug_write("BssidNum configuration value not found.") + nixio.syslog("debug","BssidNum configuration value not found.") + return + end + + for j=1,tonumber(cfgs.BssidNum) do + vifs[j] = {} + vifs[j].vifidx = j -- start from 1 + dev_idx = string.match(devname, "(%w+)") + main_ifname = l1dat and l1dat[dridx][devname].main_ifname or dbdc_prefix[mainidx][subidx].."0" + vifs[j].vifname = j == 1 and main_ifname or prefix..(j-1) + if mtkwifi.exists("/sys/class/net/"..vifs[j].vifname) then + local flags = tonumber(mtkwifi.read_pipe("cat /sys/class/net/"..vifs[j].vifname.."/flags 2>/dev/null")) or 0 + vifs[j].state = flags%2 == 1 and "up" or "down" + end + vifs[j].__ssid = cfgs["SSID"..j] + local rd_pipe_output = mtkwifi.read_pipe("cat /sys/class/net/"..prefix..(j-1).."/address 2>/dev/null") + vifs[j].__bssid = rd_pipe_output and string.match(rd_pipe_output, "%x%x:%x%x:%x%x:%x%x:%x%x:%x%x") or "?" + + vifs[j].__temp_ssid = mtkwifi.__trim(mtkwifi.read_pipe("iwconfig "..vifs[j].vifname.." | grep ESSID | cut -d : -f 2")) + vifs[j].__temp_channel = mtkwifi.read_pipe("iwconfig "..vifs[j].vifname.." | grep Channel | cut -d = -f 2 | cut -d \" \" -f 1") + if string.gsub(vifs[j].__temp_channel, "^%s*(.-)%s*$", "%1") == "" then + vifs[j].__temp_channel = mtkwifi.read_pipe("iwconfig "..vifs[j].vifname.." | grep Channel | cut -d : -f 3 | cut -d \" \" -f 1") + end + vifs[j].__wirelessmode_table = c_getWMode(vifs[j].vifname) + vifs[j].__temp_wirelessmode = vifs[j].__wirelessmode_table['getwmode'] + + if (vifs[j].__temp_ssid ~= "") then + vifs[j].__ssid = vifs[j].__temp_ssid:gsub("^\"(.-)\"$","%1") + else + vifs[j].__ssid = cfgs["SSID"..j] + end + + if (vifs[j].__temp_channel ~= "" ) then + vifs[j].__channel = vifs[j].__temp_channel + else + vifs[j].__channel = cfgs.Channel + end + + if (vifs[j].__temp_wirelessmode ~= "" and vifs[j].__temp_wirelessmode ~= "0") then + vifs[j].__wirelessmode = vifs[j].__temp_wirelessmode + else + vifs[j].__wirelessmode = mtkwifi.token_get(cfgs.WirelessMode, j, 0) + end + + vifs[j].__authmode = mtkwifi.token_get(cfgs.AuthMode, j, mtkwifi.__split(cfgs.AuthMode,";")[1]) + vifs[j].__encrypttype = mtkwifi.token_get(cfgs.EncrypType, j, mtkwifi.__split(cfgs.EncrypType,";")[1]) + vifs[j].__hidessid = mtkwifi.token_get(cfgs.HideSSID, j, mtkwifi.__split(cfgs.HideSSID,";")[1]) + vifs[j].__noforwarding = mtkwifi.token_get(cfgs.NoForwarding, j, mtkwifi.__split(cfgs.NoForwarding,";")[1]) + vifs[j].__wmmcapable = mtkwifi.token_get(cfgs.WmmCapable, j, mtkwifi.__split(cfgs.WmmCapable,";")[1]) + vifs[j].__txrate = mtkwifi.token_get(cfgs.TxRate, j, mtkwifi.__split(cfgs.TxRate,";")[1]) + vifs[j].__ieee8021x = mtkwifi.token_get(cfgs.IEEE8021X, j, mtkwifi.__split(cfgs.IEEE8021X,";")[1]) + vifs[j].__preauth = mtkwifi.token_get(cfgs.PreAuth, j, mtkwifi.__split(cfgs.PreAuth,";")[1]) + vifs[j].__rekeymethod = mtkwifi.token_get(cfgs.RekeyMethod, j, mtkwifi.__split(cfgs.RekeyMethod,";")[1]) + vifs[j].__rekeyinterval = mtkwifi.token_get(cfgs.RekeyInterval, j, mtkwifi.__split(cfgs.RekeyInterval,";")[1]) + vifs[j].__pmkcacheperiod = mtkwifi.token_get(cfgs.PMKCachePeriod, j, mtkwifi.__split(cfgs.PMKCachePeriod,";")[1]) + vifs[j].__ht_extcha = mtkwifi.token_get(cfgs.HT_EXTCHA, j, mtkwifi.__split(cfgs.HT_EXTCHA,";")[1]) + vifs[j].__radius_server = mtkwifi.token_get(cfgs.RADIUS_Server, j, mtkwifi.__split(cfgs.RADIUS_Server,";")[1]) + vifs[j].__radius_port = mtkwifi.token_get(cfgs.RADIUS_Port, j, mtkwifi.__split(cfgs.RADIUS_Port,";")[1]) + vifs[j].__wepkey_id = mtkwifi.token_get(cfgs.DefaultKeyID, j, mtkwifi.__split(cfgs.DefaultKeyID,";")[1]) + vifs[j].__wscconfmode = mtkwifi.token_get(cfgs.WscConfMode, j, mtkwifi.__split(cfgs.WscConfMode,";")[1]) + vifs[j].__wepkeys = { + cfgs["Key1Str"..j], + cfgs["Key2Str"..j], + cfgs["Key3Str"..j], + cfgs["Key4Str"..j], + } + vifs[j].__wpapsk = cfgs["WPAPSK"..j] + vifs[j].__ht_stbc = mtkwifi.token_get(cfgs.HT_STBC, j, mtkwifi.__split(cfgs.HT_STBC,";")[1]) + vifs[j].__ht_ldpc = mtkwifi.token_get(cfgs.HT_LDPC, j, mtkwifi.__split(cfgs.HT_LDPC,";")[1]) + vifs[j].__vht_stbc = mtkwifi.token_get(cfgs.VHT_STBC, j, mtkwifi.__split(cfgs.VHT_STBC,";")[1]) + vifs[j].__vht_ldpc = mtkwifi.token_get(cfgs.VHT_LDPC, j, mtkwifi.__split(cfgs.VHT_LDPC,";")[1]) + vifs[j].__dls_capable = mtkwifi.token_get(cfgs.DLSCapable, j, mtkwifi.__split(cfgs.DLSCapable,";")[1]) + vifs[j].__apsd_capable = mtkwifi.token_get(cfgs.APSDCapable, j, mtkwifi.__split(cfgs.APSDCapable,";")[1]) + vifs[j].__frag_threshold = mtkwifi.token_get(cfgs.FragThreshold, j, mtkwifi.__split(cfgs.FragThreshold,";")[1]) + vifs[j].__rts_threshold = mtkwifi.token_get(cfgs.RTSThreshold, j, mtkwifi.__split(cfgs.RTSThreshold,";")[1]) + vifs[j].__vht_sgi = mtkwifi.token_get(cfgs.VHT_SGI, j, mtkwifi.__split(cfgs.VHT_SGI,";")[1]) + vifs[j].__vht_bw_signal = mtkwifi.token_get(cfgs.VHT_BW_SIGNAL, j, mtkwifi.__split(cfgs.VHT_BW_SIGNAL,";")[1]) + vifs[j].__ht_protect = mtkwifi.token_get(cfgs.HT_PROTECT, j, mtkwifi.__split(cfgs.HT_PROTECT,";")[1]) + vifs[j].__ht_gi = mtkwifi.token_get(cfgs.HT_GI, j, mtkwifi.__split(cfgs.HT_GI,";")[1]) + vifs[j].__ht_opmode = mtkwifi.token_get(cfgs.HT_OpMode, j, mtkwifi.__split(cfgs.HT_OpMode,";")[1]) + vifs[j].__ht_amsdu = mtkwifi.token_get(cfgs.HT_AMSDU, j, mtkwifi.__split(cfgs.HT_AMSDU,";")[1]) + vifs[j].__ht_autoba = mtkwifi.token_get(cfgs.HT_AutoBA, j, mtkwifi.__split(cfgs.HT_AutoBA,";")[1]) + vifs[j].__igmp_snenable = mtkwifi.token_get(cfgs.IgmpSnEnable, j, mtkwifi.__split(cfgs.IgmpSnEnable,";")[1]) + vifs[j].__wdsenable = mtkwifi.token_get(cfgs.WdsEnable, j, mtkwifi.__split(cfgs.WdsEnable,";")[1]) + + -- VoW + vifs[j].__atc_tp = mtkwifi.token_get(cfgs.VOW_Rate_Ctrl_En, j, mtkwifi.__split(cfgs.VOW_Rate_Ctrl_En,";")[1]) + vifs[j].__atc_min_tp = mtkwifi.token_get(cfgs.VOW_Group_Min_Rate, j, mtkwifi.__split(cfgs.VOW_Group_Min_Rate,";")[1]) + vifs[j].__atc_max_tp = mtkwifi.token_get(cfgs.VOW_Group_Max_Rate, j, mtkwifi.__split(cfgs.VOW_Group_Max_Rate,";")[1]) + vifs[j].__atc_at = mtkwifi.token_get(cfgs.VOW_Airtime_Ctrl_En, j, mtkwifi.__split(cfgs.VOW_Airtime_Ctrl_En,";")[1]) + vifs[j].__atc_min_at = mtkwifi.token_get(cfgs.VOW_Group_Min_Ratio, j, mtkwifi.__split(cfgs.VOW_Group_Min_Ratio,";")[1]) + vifs[j].__atc_max_at = mtkwifi.token_get(cfgs.VOW_Group_Max_Ratio, j, mtkwifi.__split(cfgs.VOW_Group_Max_Ratio,";")[1]) + + -- TODO index by vifname + vifs[vifs[j].vifname] = vifs[j] + + -- OFDMA and MU-MIMO + vifs[j].__muofdma_dlenable = mtkwifi.token_get(cfgs.MuOfdmaDlEnable, j, mtkwifi.__split(cfgs.MuOfdmaDlEnable,";")[1]) + vifs[j].__muofdma_ulenable = mtkwifi.token_get(cfgs.MuOfdmaUlEnable, j, mtkwifi.__split(cfgs.MuOfdmaUlEnable,";")[1]) + vifs[j].__mumimo_dlenable = mtkwifi.token_get(cfgs.MuMimoDlEnable, j, mtkwifi.__split(cfgs.MuMimoDlEnable,";")[1]) + vifs[j].__mumimo_ulenable = mtkwifi.token_get(cfgs.MuMimoUlEnable, j, mtkwifi.__split(cfgs.MuMimoUlEnable,";")[1]) + + end + + return vifs +end + +function mtkwifi.__setup_apcli(cfgs, devname, mainidx, subidx) + local l1dat, l1 = mtkwifi.__get_l1dat() + local dridx = l1dat and l1.DEV_RINDEX + + local apcli = {} + local dev_idx = string.match(devname, "(%w+)") + local apcli_prefix = l1dat and l1dat[dridx][devname].apcli_ifname or + dbdc_apcli_prefix[mainidx][subidx] + + local apcli_name = apcli_prefix.."0" + + if mtkwifi.exists("/sys/class/net/"..apcli_name) then + apcli.vifname = apcli_name + apcli.devname = apcli_name + apcli.vifidx = "1" + local rd_pipe_output = mtkwifi.read_pipe("iwconfig "..apcli_name.." | grep ESSID 2>/dev/null") + local ssid = rd_pipe_output and string.match(rd_pipe_output, "ESSID:\"(.*)\"") + if not ssid or ssid == "" then + apcli.status = "Disconnected" + else + apcli.ssid = ssid + apcli.status = "Connected" + end + local flags = tonumber(mtkwifi.read_pipe("cat /sys/class/net/"..apcli_name.."/flags 2>/dev/null")) or 0 + apcli.state = flags%2 == 1 and "up" or "down" + rd_pipe_output = mtkwifi.read_pipe("cat /sys/class/net/"..apcli_name.."/address 2>/dev/null") + apcli.mac_addr = rd_pipe_output and string.match(rd_pipe_output, "%x%x:%x%x:%x%x:%x%x:%x%x:%x%x") or "?" + rd_pipe_output = mtkwifi.read_pipe("iwconfig "..apcli_name.." | grep 'Access Point' 2>/dev/null") + apcli.bssid = rd_pipe_output and string.match(rd_pipe_output, "%x%x:%x%x:%x%x:%x%x:%x%x:%x%x") or "Not-Associated" + return apcli + else + return + end +end + +function mtkwifi.__setup_eths() + local etherInfo = {} + local all_eth_devs = mtkwifi.read_pipe("ls /sys/class/net/ | grep eth | grep -v grep") + if not all_eth_devs or all_eth_devs == "" then + return + end + for ethName in string.gmatch(all_eth_devs, "(eth%d)") do + local ethInfo = {} + ethInfo['ifname'] = ethName + local flags = tonumber(mtkwifi.read_pipe("cat /sys/class/net/"..ethName.."/flags 2>/dev/null")) or 0 + ethInfo['state'] = flags%2 == 1 and "up" or "down" + ethInfo['mac_addr'] = mtkwifi.read_pipe("cat /sys/class/net/"..ethName.."/address 2>/dev/null") or "?" + table.insert(etherInfo,ethInfo) + end + return etherInfo +end + +function mtkwifi.__is_6890_project() + local str = mtkwifi.read_pipe("cat /etc/vendor_info | grep \"PLATFORM=\"") + str = string.gsub(str, "PLATFORM=", "") + if str:find("6890") then + return true + end + return false +end + +function mtkwifi.get_all_devs() + local nixio = require("nixio") + local devs = {} + local i = 1 -- dev idx + local profiles = mtkwifi.search_dev_and_profile() + local wpa_support = 0 + local wapi_support = 0 + + for devname,profile in mtkwifi.__spairs(profiles, function(a,b) return string.upper(a) < string.upper(b) end) do + local fd = io.open(profile,"r") + if not fd then + nixio.syslog("debug", "cannot find "..profile) + else + fd:close() + local cfgs = mtkwifi.load_profile(profile) + if not cfgs then + debug_write("error loading profile"..profile) + nixio.syslog("err", "error loading "..profile) + return + end + devs[i] = {} + devs[i].vifs = {} + devs[i].apcli = {} + devs[i].devname = devname + devs[i].profile = profile + local tmp = "" + tmp = string.split(devname, ".") + devs[i].maindev = tmp[1] + devs[i].mainidx = tonumber(tmp[2]) or 1 + devs[i].subdev = devname + devs[i].subidx = string.match(tmp[3] or "", "(%d+)")=="2" and 2 or 1 + devs[i].devband = tonumber(tmp[3]) + if devs[i].devband then + devs[i].multiprofile = true + devs[i].dbdc = true + devs[i].dbdcBandName = (profile:match("2[gG]") and "2.4G") or (profile:match("5[gG]") and "5G") + if not devs[i].dbdcBandName then + -- Make 1st band as 2.4G and 2nd band as 5G. + devs[i].dbdcBandName = (devs[i].devband == 1) and "2.4G" or "5G" + end + end + + devs[i].ApCliEnable = cfgs.ApCliEnable + devs[i].WirelessMode = string.split(cfgs.WirelessMode,";")[1] + devs[i].WirelessModeList = {} + for key, value in pairs(DevicePropertyMap) do + local found = string.find(string.upper(devname), string.upper(value.device)) + if found then + for k=1,#value.band do + devs[i].WirelessModeList[tonumber(value.band[k])] = WirelessModeList[tonumber(value.band[k])] + end + + if mtkwifi.__is_6890_project() then + if devs[i].dbdc then + nixio.syslog("debug", "6890 MiFi, change maxVif to 4") + devs[i].maxVif = 4 + else + nixio.syslog("debug", "6890 CPE, change maxVif to 8") + devs[i].maxVif = 8 + end + elseif devs[i].dbdc == true then + devs[i].maxVif = value.maxDBDCVif or value.maxVif/2 + else + devs[i].maxVif = value.maxVif or 16 + end + + devs[i].maxTxStream = value.maxTxStream + devs[i].maxRxStream = value.maxRxStream + devs[i].invalidChBwList = value.invalidChBwList + devs[i].isPowerBoostSupported = value.isPowerBoostSupported + devs[i].wdsBand = value.wdsBand + devs[i].mimoBand = value.mimoBand + devs[i].isMultiAPSupported = value.isMultiAPSupported + devs[i].isWPA3_192bitSupported = value.isWPA3_192bitSupported + end + end + devs[i].WscConfMode = cfgs.WscConfMode + devs[i].AuthModeList = AuthModeList + devs[i].AuthModeList_6G = AuthModeList_6G + devs[i].WpsEnableAuthModeList = WpsEnableAuthModeList + devs[i].WpsEnableAuthModeList_6G = WpsEnableAuthModeList_6G + + if wpa_support == 1 then + table.insert(devs[i].AuthModeList,"WPAPSK") + table.insert(devs[i].AuthModeList,"WPA") + end + + if wapi_support == 1 then + table.insert(devs[i].AuthModeList,"WAIPSK") + table.insert(devs[i].AuthModeList,"WAICERT") + end + devs[i].ApCliAuthModeList = ApCliAuthModeList + devs[i].EncryptionTypeList = EncryptionTypeList + devs[i].EncryptionTypeList_6G = EncryptionTypeList_6G + devs[i].Channel = tonumber(cfgs.Channel) + devs[i].DBDC_MODE = tonumber(cfgs.DBDC_MODE) + devs[i].band = devs[i].devband or mtkwifi.band(string.split(cfgs.WirelessMode,";")[1]) + + if cfgs.MUTxRxEnable then + if tonumber(cfgs.ETxBfEnCond)==1 + and tonumber(cfgs.MUTxRxEnable)==0 + and tonumber(cfgs.ITxBfEn)==0 + then devs[i].__mimo = 0 + elseif tonumber(cfgs.ETxBfEnCond)==0 + and tonumber(cfgs.MUTxRxEnable)==0 + and tonumber(cfgs.ITxBfEn)==1 + then devs[i].__mimo = 1 + elseif tonumber(cfgs.ETxBfEnCond)==1 + and tonumber(cfgs.MUTxRxEnable)==0 + and tonumber(cfgs.ITxBfEn)==1 + then devs[i].__mimo = 2 + elseif tonumber(cfgs.ETxBfEnCond)==1 + and tonumber(cfgs.MUTxRxEnable)>0 + and tonumber(cfgs.ITxBfEn)==0 + then devs[i].__mimo = 3 + elseif tonumber(cfgs.ETxBfEnCond)==1 + and tonumber(cfgs.MUTxRxEnable)>0 + and tonumber(cfgs.ITxBfEn)==1 + then devs[i].__mimo = 4 + else devs[i].__mimo = 5 + end + end + + if cfgs.HT_BW == "0" or not cfgs.HT_BW then + devs[i].__bw = "20" + elseif cfgs.HT_BW == "1" and cfgs.VHT_BW == "0" or not cfgs.VHT_BW then + if cfgs.HT_BSSCoexistence == "0" or not cfgs.HT_BSSCoexistence then + devs[i].__bw = "40" + else + devs[i].__bw = "60" -- 20/40 coexist + end + elseif cfgs.HT_BW == "1" and cfgs.VHT_BW == "1" then + devs[i].__bw = "80" + elseif cfgs.HT_BW == "1" and cfgs.VHT_BW == "2" then + devs[i].__bw = "160" + elseif cfgs.HT_BW == "1" and cfgs.VHT_BW == "3" then + devs[i].__bw = "161" + end + + devs[i].vifs = mtkwifi.__setup_vifs(cfgs, devname, devs[i].mainidx, devs[i].subidx) + devs[i].apcli = mtkwifi.__setup_apcli(cfgs, devname, devs[i].mainidx, devs[i].subidx) + + if mtkwifi.exists("cat /etc/wireless/"..devs[i].maindev.."/version") then + local version = mtkwifi.read_pipe("cat /etc/wireless/"..devs[i].maindev.."/version 2>/dev/null") + devs[i].version = (type(version) == "string" and version ~= "") and version or "Unknown: Empty version file!" + else + local vif_name = nil + if devs[i].apcli and devs[i].apcli["state"] == "up" then + vif_name = devs[i].apcli["vifname"] + elseif devs[i].vifs then + for _,vif in ipairs(devs[i].vifs) do + if vif["state"] == "up" then + vif_name = vif["vifname"] + break + end + end + end + if not vif_name then + if tonumber(cfgs.BssidNum) >= 1 then + devs[i].version = "Enable an interface to get the driver version." + elseif devs[i].apcli and devs[i].apcli["state"] ~= "up" then + devs[i].version = "Enable ApCli interface i.e. "..devs[i].apcli["vifname"].." to get the driver version." + else + devs[i].version = "Add an interface to get the driver version." + end + else + local version = mtkwifi.read_pipe("iwpriv "..vif_name.." get_driverinfo") + version = version and version:match("Driver version: (.-)\n") or "" + devs[i].version = version ~= "" and version or "Unknown: Incorrect response from version command!" + end + end + + -- Setup reverse indices by devname + devs[devname] = devs[i] + + if devs[i].apcli then + devs[i][devs[i].apcli.devname] = devs[i].apcli + end + + i = i + 1 + end + end + devs['etherInfo'] = mtkwifi.__setup_eths() + return devs +end + +function mtkwifi.exists(path) + local fp = io.open(path, "rb") + if fp then fp:close() end + return fp ~= nil +end + +function mtkwifi.parse_mac(str) + local macs = {} + local pat = "^[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]:[0-9a-fA-F][0-9a-fA-F]$" + + local function ismac(str) + if str:match(pat) then return str end + end + + if not str then return macs end + local t = str:split("\n") + for _,v in pairs(t) do + local mac = ismac(mtkwifi.__trim(v)) + if mac then + table.insert(macs, mac) + end + end + + return macs + -- body +end + + +function mtkwifi.scan_ap(vifname) + os.execute("iwpriv "..vifname.." set SiteSurvey=0") + os.execute("sleep 10") -- depends on your env + local op = c_scanResult(vifname, 0) + local scan_result = op["scanresult"] + local next_line_index = 0 + local cur_index + local total_index = 0 + local ap_list = {} + local xx = {} + local tmp + + while (1) do + for i, line in ipairs(mtkwifi.__lines(scan_result)) do + local is_mac_addr_present = string.match(line, "%s+%x%x:%x%x:%x%x:%x%x:%x%x:%x%x%s+") + -- If the line does not contain any MAC address and length is greater than 40 bytes, + -- then, the line is the header of the get_site_survey page. + local total_str = string.find(line, "Total=") + if total_str == 1 then + total_index = tonumber(line:match("%d+")) + end + + if #line>40 and not is_mac_addr_present then + xx.Ch = {string.find(line, "Ch "),3} + xx.SSID = {string.find(line, "SSID "),32} + local fidx = string.find(line, "SSID_Len") + if fidx then + xx.SSID_len = {fidx,2} + end + xx.BSSID = {string.find(line, "BSSID "),17} + xx.Security = {string.find(line, "Security "),22} + xx.Signal = {string.find(line, "Sig%a%al"),4} + xx.Mode = {string.find(line, "W-Mode"),5} + xx.ExtCh = {string.find(line, "ExtCH"),6} + xx.WPS = {string.find(line, "WPS"),3} + xx.NT = {string.find(line, "NT"),2} + fidx = string.find(line, "OWETranIe") + if fidx then + xx.OWETranIe = {fidx,9} + end + end + + if #line>40 and is_mac_addr_present then + tmp = {} + tmp.channel = mtkwifi.__trim(string.sub(line, xx.Ch[1], xx.Ch[1]+xx.Ch[2])) + if xx.SSID_len then + -- Maximum xx.SSID[2] characters are supported in SSID + tmp.ssid_len = tonumber(mtkwifi.__trim(string.sub(line, xx.SSID_len[1], xx.SSID_len[1]+xx.SSID_len[2]))) or xx.SSID[2] + if tmp.ssid_len > xx.SSID[2] or tmp.ssid_len < 0 then + tmp.ssid_len = xx.SSID[2] + tmp.ssid = string.sub(line, xx.SSID[1], xx.SSID[1]+tmp.ssid_len-1) + else + tmp.ssid = string.sub(line, xx.SSID[1], xx.BSSID[1]-1) + if string.find(tmp.ssid, "0x") == nil then + tmp.ssid = string.sub(line, xx.SSID[1], xx.SSID[1]+tmp.ssid_len-1) + end + end + else + tmp.ssid = mtkwifi.__trim(string.sub(line, xx.SSID[1], xx.SSID[1]+xx.SSID[2])) + tmp.ssid_len = tmp.ssid:len() + end + tmp.bssid = string.upper(mtkwifi.__trim(string.sub(line, xx.BSSID[1], xx.BSSID[1]+xx.BSSID[2]))) + tmp.security = mtkwifi.__trim(string.sub(line, xx.Security[1], xx.Security[1]+xx.Security[2])) + tmp.authmode = mtkwifi.__trim(string.split(tmp.security, "/")[1]) + tmp.encrypttype = mtkwifi.__trim(string.split(tmp.security, "/")[2] or "NONE") + tmp.rssi = mtkwifi.__trim(string.sub(line, xx.Signal[1], xx.Signal[1]+xx.Signal[2])) + tmp.extch = mtkwifi.__trim(string.sub(line, xx.ExtCh[1], xx.ExtCh[1]+xx.ExtCh[2])) + tmp.mode = mtkwifi.__trim(string.sub(line, xx.Mode[1], xx.Mode[1]+xx.Mode[2])) + tmp.wps = mtkwifi.__trim(string.sub(line, xx.WPS[1], xx.WPS[1]+xx.WPS[2])) + tmp.nt = mtkwifi.__trim(string.sub(line, xx.NT[1], xx.NT[1]+xx.NT[2])) + if xx.OWETranIe then + tmp.OWETranIe = mtkwifi.__trim(string.sub(line, xx.OWETranIe[1], xx.OWETranIe[1]+xx.OWETranIe[2])) + end + table.insert(ap_list, tmp) + cur_index = tonumber(line:match("^%d+")) + if cur_index == total_index - 1 then + break; + end + next_line_index = cur_index and cur_index + 1 or next_line_index + end + end + if cur_index and cur_index == next_line_index - 1 then + --scan_result = mtkwifi.read_pipe("iwpriv "..vifname.." get_site_survey "..next_line_index) + if next_line_index == total_index - 1 then + scan_result = nil + else + op = c_scanResult(vifname, next_line_index) + scan_result = op["scanresult"] + end + else + scan_result = nil + end + + if not scan_result or not string.match(scan_result, "%s+%x%x:%x%x:%x%x:%x%x:%x%x:%x%x%s+") then + break + end + end + + return ap_list +end + +function mtkwifi.__any_wsc_enabled(wsc_conf_mode) + if (wsc_conf_mode == "") then + return 0; + end + if (wsc_conf_mode == "7") then + return 1; + end + if (wsc_conf_mode == "4") then + return 1; + end + if (wsc_conf_mode == "2") then + return 1; + end + if (wsc_conf_mode == "1") then + return 1; + end + return 0; +end + +function mtkwifi.__restart_if_wps(devname, ifname, cfgs) + local devs = mtkwifi.get_all_devs() + local ssid_index = devs[devname]["vifs"][ifname].vifidx + local wsc_conf_mode = "" + + wsc_conf_mode=mtkwifi.token_get(cfgs["WscConfMode"], ssid_index, "") + + os.execute("iwpriv "..ifname.." set WscConfMode=0") + debug_write("iwpriv "..ifname.." set WscConfMode=0") + os.execute("route delete 239.255.255.250") + debug_write("route delete 239.255.255.250") + if(mtkwifi.__any_wsc_enabled(wsc_conf_mode)) then + os.execute("iwpriv "..ifname.." set WscConfMode=7") + debug_write("iwpriv "..ifname.." set WscConfMode=7") + os.execute("route add -host 239.255.255.250 dev br0") + debug_write("route add -host 239.255.255.250 dev br0") + end + + -- execute wps_action.lua file to send signal for current interface + os.execute("lua wps_action.lua "..ifname) + debug_write("lua wps_action.lua "..ifname) + return cfgs +end + +function mtkwifi.restart_8021x(devname, devices) + local l1dat, l1 = mtkwifi.__get_l1dat() + local dridx = l1dat and l1.DEV_RINDEX + + local devs = devices or mtkwifi.get_all_devs() + local dev = devs[devname] + local main_ifname = l1dat and l1dat[dridx][devname].main_ifname or dbdc_prefix[mainidx][subidx].."0" + local prefix = l1dat and l1dat[dridx][devname].ext_ifname or dbdc_prefix[mainidx][subidx] + + local ps_cmd = "ps | grep -v grep | grep rt2860apd | grep "..main_ifname.." | awk '{print $1}'" + local pid_cmd = "cat /var/run/rt2860apd_"..devs[devname].vifs[1].vifname..".pid" + local apd_pid = mtkwifi.read_pipe(pid_cmd) or mtkwifi.read_pipe(ps_cmd) + if tonumber(apd_pid) then + os.execute("kill "..apd_pid) + end + + local cfgs = mtkwifi.load_profile(devs[devname].profile) + local auth_mode = cfgs['AuthMode'] + local ieee8021x = cfgs['IEEE8021X'] + local pat_auth_mode = {"WPA$", "WPA;", "WPA2$", "WPA2;", "WPA1WPA2$", "WPA1WPA2;"} + local pat_ieee8021x = {"1$", "1;"} + local apd_en = false + + for _, pat in ipairs(pat_auth_mode) do + if string.find(auth_mode, pat) then + apd_en = true + end + end + + for _, pat in ipairs(pat_ieee8021x) do + if string.find(ieee8021x, pat) then + apd_en = true + end + end + + if not apd_en then + return + end + if prefix == "ra" then + mtkwifi.__fork_exec("rt2860apd -i "..main_ifname.." -p "..prefix) + elseif prefix == "rae" then + mtkwifi.__fork_exec("rtwifi3apd -i "..main_ifname.." -p "..prefix) + elseif prefix == "rai" then + mtkwifi.__fork_exec("rtinicapd -i "..main_ifname.." -p "..prefix) + elseif prefix == "rax" or prefix == "ray" or prefix == "raz" then + mtkwifi.__fork_exec("rt2860apd_x -i "..main_ifname.." -p "..prefix) + end +end + +function mtkwifi.dat2uci(datfile, ucifile) + local shuci = require("shuci") + local cfgs = mtkwifi.load_profile(datfile) + + local uci = {} + + uci["wifi-device"]={} + uci["wifi-device"][".name"] = device + uci["wifi-device"]["type"] = device + uci["wifi-device"]["vendor"] = "ralink" + uci["wifi-device"]["iface"] = {} + + local i = 1 -- index of wifi-iface + + uci["iface"] = {} + while i <= tonumber(cfgs.BssidNum) do + uci["iface"][i] = {} + local iface = uci["iface"][i] + iface["ssid"] = cfgs["SSID"..(i)] + iface["mode"] = "ap" + iface["network"] = "lan" + iface["ifname"] = "ra0" + iface[".name"] = device.."."..iface["ifname"] + + i=i+1 + end + + shuci.encode(uci, ucifile) +end + +function mtkwifi.uci2dat(ucifile, devname, datfile) + local shuci = require("shuci") + local uci = shuci.decode(ucifile) + local cfgs = mtkwifi.load_profile(datfile) or {} + + if not ucifile or not devname then return end + + for _,dev in ipairs(uci["wifi-device"][devname]) do + for k,v in pairs(dev) do + if string.byte(k) ~= string.byte(".") + and string.byte(k) ~= string.byte("_") then + cfgs.k = v + end + end + end + if datfile then + save_profile(cfgs, datfile) + end +end + +function mtkwifi.get_referer_url() + local to_url + local script_name = luci.http.getenv('SCRIPT_NAME') + local http_referer = luci.http.getenv('HTTP_REFERER') + if script_name and http_referer then + local fIdx = http_referer:find(script_name,1,true) + if fIdx then + to_url = http_referer:sub(fIdx) + end + end + if not to_url or to_url == "" then + to_url = luci.dispatcher.build_url("admin", "mtk", "wifi") + end + return to_url +end + +function mtkwifi.save_read_easymesh_profile(easymesh_cfgs) + if not easymesh_cfgs then + return + end + local easymesh_applied_path = mtkwifi.__profile_applied_settings_path(mtkwifi.__read_easymesh_profile_path()) + if not mtkwifi.exists(easymesh_applied_path) then + os.execute("cp -f "..mtkwifi.__read_easymesh_profile_path().." "..easymesh_applied_path) + end + + local fd = io.open(mtkwifi.__read_easymesh_profile_path(), "w") + if not fd then return end + table.sort(easymesh_cfgs, function(a,b) return avalue:"..v..",") + end + fd:close() + + mtkwifi.save_easymesh_profile_to_nvram() + os.execute("sync >/dev/null 2>&1") +end + +function mtkwifi.save_easymesh_profile_to_nvram() + if not pcall(require, "mtknvram") then + return + end + local nvram = require("mtknvram") + local merged_easymesh_dev1_path = "/tmp/mtk/wifi/merged_easymesh_dev1.dat" + local l1dat, l1 = mtkwifi.__get_l1dat() + local dev1_profile_paths + local dev1_profile_path_table = l1 and l1.l1_zone_to_path("dev1") + if not next(dev1_profile_path_table) then + return + end + dev1_profile_paths = table.concat(dev1_profile_path_table, " ") + -- Uncomment below two statements when there is sufficient space in dev1 NVRAM zone to store EasyMesh Agent's BSS Cfgs Settings. + -- mtkwifi.__prepare_easymesh_bss_nvram_cfgs() + -- os.execute("cat "..dev1_profile_paths.." "..mtkwifi.__read_easymesh_profile_path().." "..mtkwifi.__easymesh_bss_cfgs_nvram_path().." > "..merged_easymesh_dev1_path.." 2>/dev/null") + -- Comment or remove below line once above requirement is met. + os.execute("cat "..dev1_profile_paths.." "..mtkwifi.__read_easymesh_profile_path().." > "..merged_easymesh_dev1_path.." 2>/dev/null") + nvram.nvram_save_profile(merged_easymesh_dev1_path, "dev1") + os.execute("sync >/dev/null 2>&1") +end + +function mtkwifi.save_easymesh_mapd_profile(easymesh_mapd_cfgs) + if not easymesh_mapd_cfgs then + return + end + local fd = io.open(mtkwifi.__easymesh_mapd_profile_path(), "w") + if not fd then return end + table.sort(easymesh_mapd_cfgs, function(a,b) return a/dev/null 2>&1") +end + +function mtkwifi.save_write_easymesh_profile(easymesh_mapd_cfgs) + if not easymesh_mapd_cfgs then + return + end + local fd = io.open(mtkwifi.__write_easymesh_profile_path(), "w") + if not fd then return end + table.sort(easymesh_mapd_cfgs, function(a,b) return a/dev/null 2>&1") +end + +function mtkwifi.__read_easymesh_profile_path() + return "/etc/map/mapd_cfg" +end + +function mtkwifi.__write_easymesh_profile_path() + return "/etc/map/mapd_user.cfg" +end + +function mtkwifi.__easymesh_mapd_profile_path() + return "/etc/mapd_strng.conf" +end + +function mtkwifi.__easymesh_bss_cfgs_path() + return "/etc/map/wts_bss_info_config" +end + +function mtkwifi.__easymesh_bss_cfgs_nvram_path() + local p = "/tmp/mtk/wifi/wts_bss_info_config.nvram" + os.execute("mkdir -p /tmp/mtk/wifi") + return p +end + +function mtkwifi.get_easymesh_al_mac(devRole) + local r = {} + local mapd_app_cfgs = mtkwifi.load_profile("/etc/map/1905d.cfg") + if not mapd_app_cfgs then + r['status'] = "Failed to load /etc/map/1905d.cfg file!" + else + r['status'] = 'SUCCESS' + if tonumber(devRole) == 1 then + r['al_mac'] = mapd_app_cfgs['map_controller_alid'] + else + r['al_mac'] = mapd_app_cfgs['map_agent_alid'] + end + --local easymesh_cfgs = mtkwifi.load_profile(mtkwifi.__read_easymesh_profile_path()) + --if easymesh_cfgs['MapAlMac'] ~= r['al_mac'] then + -- easymesh_cfgs['MapAlMac'] = r['al_mac'] + -- mtkwifi.save_write_easymesh_profile(easymesh_cfgs) + --end + end + return r +end + +function mtkwifi.get_easymesh_on_boarded_iface_info() + local r = {} + r['status'] = "ERROR" + r['staBhInfStr'] = "" + r['profile'] = "" + local devs = mtkwifi.get_all_devs() + for _, dev in ipairs(devs) do + if dev.apcli and dev.apcli.status == "Connected" then + r['status'] = "SUCCESS" + r['staBhInfStr'] = r['staBhInfStr']..dev.apcli.vifname..';' + r['profile'] = r['profile']..dev.profile..';' + end + end + return r +end + +function mtkwifi.load_easymesh_bss_cfgs() + local fd = io.open(mtkwifi.__easymesh_bss_cfgs_path(), "r") + if not fd then + return + end + local content = fd:read("*all") + fd:close() + + local cfgs = {} + cfgs['wildCardAlMacCfgs'] = {} + cfgs['distinctAlMacCfgs'] = {} + local tmp = {} + + -- convert profile into lua table + for _,line in ipairs(mtkwifi.__lines(content)) do + -- Trim only leading space characters + line = line:gsub("^%s*(.-)$","%1") + if string.byte(line) ~= string.byte("#") then + local b,e,lineNo,alMac,band = string.find(line, "^(%d+),(%x%x:%x%x:%x%x:%x%x:%x%x:%x%x)%s+(%d+x)%s+") + if band then + alMac = alMac:upper() + local bssInfoIdx + if tmp[alMac] then + if tmp[alMac][band] then + bssInfoIdx = mtkwifi.get_table_length(tmp[alMac][band]) + 1 + tmp[alMac][band][bssInfoIdx] = {} + else + bssInfoIdx = 1 + tmp[alMac][band] = {} + tmp[alMac][band][bssInfoIdx] = {} + end + else + bssInfoIdx = 1 + tmp[alMac] = {} + tmp[alMac][band] = {} + tmp[alMac][band][bssInfoIdx] = {} + end + local tokIdx, token = 0, nil + local bssLineStr = line:sub(e+1) + local ssid = string.gsub(bssLineStr, "(%s0x%d+).*", "") + tmp[alMac][band][bssInfoIdx]['ssid'] = ssid + local security = string.match(bssLineStr, "0x%S+ 0x%S+") + tmp[alMac][band][bssInfoIdx]['authMode'] = security:sub(1,6) + tmp[alMac][band][bssInfoIdx]['encType'] = security:sub(8,13) + local newBssLineStr = string.match(bssLineStr, "0x%S+ %S.*") + local updateBssLineStr = string.gsub(newBssLineStr, "0x%S+ 0x%S+%s", "") + local passPhrase = string.gsub(updateBssLineStr, "%s%d %d %S+ %d+ %S+ %S+", "") + tmp[alMac][band][bssInfoIdx]['passPhrase'] = passPhrase + local restBssLineStr = string.match(updateBssLineStr, "%d %d %S+ %d+ %S+ %S+") + for token in string.gmatch(restBssLineStr, "(%S+)%s?") do + tokIdx = tokIdx + 1 + if tokIdx == 1 then + tmp[alMac][band][bssInfoIdx]['isBhBssSupported'] = token + elseif tokIdx == 2 then + tmp[alMac][band][bssInfoIdx]['isFhBssSupported'] = token + elseif tokIdx == 3 then + tmp[alMac][band][bssInfoIdx]['isHidden'] = token + elseif tokIdx == 4 then + tmp[alMac][band][bssInfoIdx]['fhVlanId'] = token + elseif tokIdx == 5 then + tmp[alMac][band][bssInfoIdx]['primVlan'] = token + elseif tokIdx == 6 then + tmp[alMac][band][bssInfoIdx]['defPCP'] = token + else + nixio.syslog("warning", "load_easymesh_bss_cfgs: Extra Unknown Parameters "..line) + end + end + if tokIdx == 6 then + if alMac == "FF:FF:FF:FF:FF:FF" then + cfgs['wildCardAlMacCfgs']['FF:FF:FF:FF:FF:FF'] = tmp[alMac] + else + cfgs['distinctAlMacCfgs'][alMac] = tmp[alMac] + end + else + tmp[alMac][band][bssInfoIdx] = nil + nixio.syslog("warning", "load_easymesh_bss_cfgs: skip invalid line "..line) + end + else + nixio.syslog("warning", "load_easymesh_bss_cfgs: skip line without 'LineNumber,AL-MAC Band' "..line) + end + else + nixio.syslog("warning", "load_easymesh_bss_cfgs: skip comment line "..line) + end + end + return cfgs +end + +function mtkwifi.save_easymesh_bss_cfgs(cfgs) + if not cfgs or not cfgs['wildCardAlMacCfgs'] or not cfgs['distinctAlMacCfgs'] then + return + end + local easymesh_bss_cfg_applied_path = mtkwifi.__profile_applied_settings_path(mtkwifi.__easymesh_bss_cfgs_path()) + if not mtkwifi.exists(easymesh_bss_cfg_applied_path) then + os.execute("cp -f "..mtkwifi.__easymesh_bss_cfgs_path().." "..easymesh_bss_cfg_applied_path) + end + + local fd = io.open(mtkwifi.__easymesh_bss_cfgs_path(), "w") + if not fd then + return + end + + local lineIdx = 0 + -- First write distinct AL-MAC cfgs; then write wildcard AL-MAC(FF:FF:FF:FF:FF:FF) cfgs + for alMac,alMacTbl in pairs(cfgs['distinctAlMacCfgs']) do + for band,bssInfoTbl in pairs(alMacTbl) do + for _,bssInfo in pairs(bssInfoTbl) do + lineIdx = lineIdx + 1 + fd:write(lineIdx..','..alMac..' '.. + band..' '.. + bssInfo['ssid']..' '.. + bssInfo['authMode']..' '.. + bssInfo['encType']..' '.. + bssInfo['passPhrase']..' '.. + bssInfo['isBhBssSupported']..' '.. + bssInfo['isFhBssSupported']..' '.. + bssInfo['isHidden']..' '.. + bssInfo['fhVlanId']..' '.. + bssInfo['primVlan']..' '.. + bssInfo['defPCP'].. + '\n') + end + end + end + for alMac,alMacTbl in pairs(cfgs['wildCardAlMacCfgs']) do + for band,bssInfoTbl in pairs(alMacTbl) do + for _,bssInfo in pairs(bssInfoTbl) do + lineIdx = lineIdx + 1 + fd:write(lineIdx..','..alMac..' '.. + band..' '.. + bssInfo['ssid']..' '.. + bssInfo['authMode']..' '.. + bssInfo['encType']..' '.. + bssInfo['passPhrase']..' '.. + bssInfo['isBhBssSupported']..' '.. + bssInfo['isFhBssSupported']..' '.. + bssInfo['isHidden']..' '.. + bssInfo['fhVlanId']..' '.. + bssInfo['primVlan']..' '.. + bssInfo['defPCP'].. + '\n') + end + end + end + fd:close() + os.execute("sync "..mtkwifi.__easymesh_bss_cfgs_path().." >/dev/null 2>&1") + + -- Uncomment below line when there is sufficient space in dev1 NVRAM zone to store EasyMesh Agent's BSS Cfgs Settings. + -- mtkwifi.save_easymesh_profile_to_nvram() +end + +function mtkwifi.__prepare_easymesh_bss_nvram_cfgs() + local fd = io.open(mtkwifi.__easymesh_bss_cfgs_nvram_path(), "w") + if not fd then + return + end + local cfgs = mtkwifi.load_easymesh_bss_cfgs() + local lineIdx = 0 + -- First write distinct AL-MAC cfgs; then write wildcard AL-MAC(FF:FF:FF:FF:FF:FF) cfgs + for alMac,alMacTbl in pairs(cfgs['distinctAlMacCfgs']) do + for band,bssInfoTbl in pairs(alMacTbl) do + for _,bssInfo in pairs(bssInfoTbl) do + lineIdx = lineIdx + 1 + fd:write('EasyMeshBssCfgsLine'..lineIdx..'='..lineIdx..','..alMac..' '.. + band..' '.. + bssInfo['ssid']..' '.. + bssInfo['authMode']..' '.. + bssInfo['encType']..' '.. + bssInfo['passPhrase']..' '.. + bssInfo['isBhBssSupported']..' '.. + bssInfo['isFhBssSupported']..' '.. + bssInfo['isHidden']..' '.. + bssInfo['fhVlanId']..' '.. + bssInfo['primVlan']..' '.. + bssInfo['defPCP'].. + '\n') + end + end + end + for alMac,alMacTbl in pairs(cfgs['wildCardAlMacCfgs']) do + for band,bssInfoTbl in pairs(alMacTbl) do + for _,bssInfo in pairs(bssInfoTbl) do + lineIdx = lineIdx + 1 + fd:write('EasyMeshBssCfgsLine'..lineIdx..'='..lineIdx..','..alMac..' '.. + band..' '.. + bssInfo['ssid']..' '.. + bssInfo['authMode']..' '.. + bssInfo['encType']..' '.. + bssInfo['passPhrase']..' '.. + bssInfo['isBhBssSupported']..' '.. + bssInfo['isFhBssSupported']..' '.. + bssInfo['isHidden']..' '.. + bssInfo['fhVlanId']..' '.. + bssInfo['primVlan']..' '.. + bssInfo['defPCP'].. + '\n') + end + end + end + fd:write('EasyMeshTotalBssCfgsLines='..lineIdx..'\n') + fd:close() + os.execute("sync >/dev/null 2>&1") +end + +return mtkwifi diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/mtkwifi.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/mtkwifi.luac new file mode 100644 index 000000000000..d9d87499d339 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/mtkwifi.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/nixio/fs.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/nixio/fs.lua new file mode 100644 index 000000000000..8883835f3037 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/nixio/fs.lua @@ -0,0 +1,175 @@ +--[[ +nixio - Linux I/O library for lua + +Copyright 2009 Steven Barth + +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 + +http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +local table = require "table" +local nixio = require "nixio" +local type, ipairs, setmetatable = type, ipairs, setmetatable +require "nixio.util" + + +module ("nixio.fs", function(m) setmetatable(m, {__index = nixio.fs}) end) + + +function readfile(path, limit) + local fd, code, msg = nixio.open(path, "r") + local data + if not fd then + return nil, code, msg + end + + data, code, msg = fd:readall(limit) + + fd:close() + return data, code, msg +end + + +function writefile(path, data) + local fd, code, msg, stat = nixio.open(path, "w") + if not fd then + return nil, code, msg + end + + stat, code, msg = fd:writeall(data) + + fd:close() + return stat, code, msg +end + +function datacopy(src, dest, size) + local fdin, code, msg = nixio.open(src, "r") + if not fdin then + return nil, code, msg + end + + local fdout, code, msg = nixio.open(dest, "w") + if not fdout then + return nil, code, msg + end + + local stat, code, msg, sent = fdin:copy(fdout, size) + fdin:close() + fdout:close() + + return stat, code, msg, sent +end + +function copy(src, dest) + local stat, code, msg, res = nixio.fs.lstat(src) + if not stat then + return nil, code, msg + end + + if stat.type == "dir" then + if nixio.fs.stat(dest, type) ~= "dir" then + res, code, msg = nixio.fs.mkdir(dest) + else + stat = true + end + elseif stat.type == "lnk" then + res, code, msg = nixio.fs.symlink(nixio.fs.readlink(src), dest) + elseif stat.type == "reg" then + res, code, msg = datacopy(src, dest) + end + + if not res then + return nil, code, msg + end + + nixio.fs.utimes(dest, stat.atime, stat.mtime) + + if nixio.fs.lchown then + nixio.fs.lchown(dest, stat.uid, stat.gid) + end + + if stat.type ~= "lnk" then + nixio.fs.chmod(dest, stat.modedec) + end + + return true +end + +function move(src, dest) + local stat, code, msg = nixio.fs.rename(src, dest) + if not stat and code == nixio.const.EXDEV then + stat, code, msg = copy(src, dest) + if stat then + stat, code, msg = nixio.fs.unlink(src) + end + end + return stat, code, msg +end + +function mkdirr(dest, mode) + if nixio.fs.stat(dest, "type") == "dir" then + return true + else + local stat, code, msg = nixio.fs.mkdir(dest, mode) + if not stat and code == nixio.const.ENOENT then + stat, code, msg = mkdirr(nixio.fs.dirname(dest), mode) + if stat then + stat, code, msg = nixio.fs.mkdir(dest, mode) + end + end + return stat, code, msg + end +end + +local function _recurse(cb, src, dest) + local type = nixio.fs.lstat(src, "type") + if type ~= "dir" then + return cb(src, dest) + else + local stat, se, code, msg, s, c, m = true, nixio.const.sep + if dest then + s, c, m = cb(src, dest) + stat, code, msg = stat and s, c or code, m or msg + end + + for e in nixio.fs.dir(src) do + if dest then + s, c, m = _recurse(cb, src .. se .. e, dest .. se .. e) + else + s, c, m = _recurse(cb, src .. se .. e) + end + stat, code, msg = stat and s, c or code, m or msg + end + + if not dest then -- Postfix + s, c, m = cb(src) + stat, code, msg = stat and s, c or code, m or msg + end + + return stat, code, msg + end +end + +function copyr(src, dest) + return _recurse(copy, src, dest) +end + +function mover(src, dest) + local stat, code, msg = nixio.fs.rename(src, dest) + if not stat and code == nixio.const.EXDEV then + stat, code, msg = _recurse(copy, src, dest) + if stat then + stat, code, msg = _recurse(nixio.fs.remove, src) + end + end + return stat, code, msg +end + +function remover(src) + return _recurse(nixio.fs.remove, src) +end \ No newline at end of file diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/nixio/fs.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/nixio/fs.luac new file mode 100644 index 000000000000..554fa66c6514 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/nixio/fs.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/nixio/util.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/nixio/util.lua new file mode 100644 index 000000000000..63d2f6214796 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/nixio/util.lua @@ -0,0 +1,270 @@ +--[[ +nixio - Linux I/O library for lua + +Copyright 2009 Steven Barth + +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 + +http://www.apache.org/licenses/LICENSE-2.0 + +$Id$ +]]-- + +local table = require "table" +local nixio = require "nixio" +local getmetatable, assert, pairs, type = getmetatable, assert, pairs, type +local tostring = tostring + +module "nixio.util" + +local BUFFERSIZE = nixio.const.buffersize +local ZIOBLKSIZE = 65536 +local socket = nixio.meta_socket +local tls_socket = nixio.meta_tls_socket +local file = nixio.meta_file +local uname = nixio.uname() +local ZBUG = uname.sysname == "Linux" and uname.release:sub(1, 3) == "2.4" + +function consume(iter, append) + local tbl = append or {} + if iter then + for obj in iter do + tbl[#tbl+1] = obj + end + end + return tbl +end + +local meta = {} + +function meta.is_socket(self) + return (getmetatable(self) == socket) +end + +function meta.is_tls_socket(self) + return (getmetatable(self) == tls_socket) +end + +function meta.is_file(self) + return (getmetatable(self) == file) +end + +function meta.readall(self, len) + local block, code, msg = self:read(len or BUFFERSIZE) + + if not block then + return nil, code, msg, "" + elseif #block == 0 then + return "", nil, nil, "" + end + + local data, total = {block}, #block + + while not len or len > total do + block, code, msg = self:read(len and (len - total) or BUFFERSIZE) + + if not block then + return nil, code, msg, table.concat(data) + elseif #block == 0 then + break + end + + data[#data+1], total = block, total + #block + end + + local data = #data > 1 and table.concat(data) or data[1] + return data, nil, nil, data +end +meta.recvall = meta.readall + +function meta.writeall(self, data) + data = tostring(data) + local sent, code, msg = self:write(data) + + if not sent then + return nil, code, msg, 0 + end + + local total = sent + + while total < #data do + sent, code, msg = self:write(data, total) + + if not sent then + return nil, code, msg, total + end + + total = total + sent + end + + return total, nil, nil, total +end +meta.sendall = meta.writeall + +function meta.linesource(self, limit) + limit = limit or BUFFERSIZE + local buffer = "" + local bpos = 0 + return function(flush) + local line, endp, _ + + if flush then + line = buffer:sub(bpos + 1) + buffer = type(flush) == "string" and flush or "" + bpos = 0 + return line + end + + while not line do + _, endp, line = buffer:find("(.-)\r?\n", bpos + 1) + if line then + bpos = endp + return line + elseif #buffer < limit + bpos then + local newblock, code, msg = self:read(limit + bpos - #buffer) + if not newblock then + return nil, code, msg + elseif #newblock == 0 then + return nil + end + buffer = buffer:sub(bpos + 1) .. newblock + bpos = 0 + else + return nil, 0 + end + end + end +end + +function meta.blocksource(self, bs, limit) + bs = bs or BUFFERSIZE + return function() + local toread = bs + if limit then + if limit < 1 then + return nil + elseif limit < toread then + toread = limit + end + end + + local block, code, msg = self:read(toread) + + if not block then + return nil, code, msg + elseif #block == 0 then + return nil + else + if limit then + limit = limit - #block + end + + return block + end + end +end + +function meta.sink(self, close) + return function(chunk, src_err) + if not chunk and not src_err and close then + if self.shutdown then + self:shutdown() + end + self:close() + elseif chunk and #chunk > 0 then + return self:writeall(chunk) + end + return true + end +end + +function meta.copy(self, fdout, size) + local source = self:blocksource(nil, size) + local sink = fdout:sink() + local sent, chunk, code, msg = 0 + + repeat + chunk, code, msg = source() + sink(chunk, code, msg) + sent = chunk and (sent + #chunk) or sent + until not chunk + return not code and sent or nil, code, msg, sent +end + +function meta.copyz(self, fd, size) + local sent, lsent, code, msg = 0 + local splicable + + if not ZBUG and self:is_file() then + local ftype = self:stat("type") + if nixio.sendfile and fd:is_socket() and ftype == "reg" then + repeat + lsent, code, msg = nixio.sendfile(fd, self, size or ZIOBLKSIZE) + if lsent then + sent = sent + lsent + size = size and (size - lsent) + end + until (not lsent or lsent == 0 or (size and size == 0)) + if lsent or (not lsent and sent == 0 and + code ~= nixio.const.ENOSYS and code ~= nixio.const.EINVAL) then + return lsent and sent, code, msg, sent + end + elseif nixio.splice and not fd:is_tls_socket() and ftype == "fifo" then + splicable = true + end + end + + if nixio.splice and fd:is_file() and not splicable then + splicable = not self:is_tls_socket() and fd:stat("type") == "fifo" + end + + if splicable then + repeat + lsent, code, msg = nixio.splice(self, fd, size or ZIOBLKSIZE) + if lsent then + sent = sent + lsent + size = size and (size - lsent) + end + until (not lsent or lsent == 0 or (size and size == 0)) + if lsent or (not lsent and sent == 0 and + code ~= nixio.const.ENOSYS and code ~= nixio.const.EINVAL) then + return lsent and sent, code, msg, sent + end + end + + return self:copy(fd, size) +end + +if tls_socket then + function tls_socket.close(self) + return self.socket:close() + end + + function tls_socket.getsockname(self) + return self.socket:getsockname() + end + + function tls_socket.getpeername(self) + return self.socket:getpeername() + end + + function tls_socket.getsockopt(self, ...) + return self.socket:getsockopt(...) + end + tls_socket.getopt = tls_socket.getsockopt + + function tls_socket.setsockopt(self, ...) + return self.socket:setsockopt(...) + end + tls_socket.setopt = tls_socket.setsockopt +end + +for k, v in pairs(meta) do + file[k] = v + socket[k] = v + if tls_socket then + tls_socket[k] = v + end +end diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/nixio/util.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/nixio/util.luac new file mode 100644 index 000000000000..72364bd97243 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/nixio/util.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/shuci.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/shuci.lua new file mode 100755 index 000000000000..c90e06f82a16 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/shuci.lua @@ -0,0 +1,128 @@ +#!/usr/bin/env lua + +--[[ + * A pure lua library to translate between lua table and uci config + * + * For UCI: http://wiki.openwrt.org/doc/techref/uci + * http://wiki.openwrt.org/doc/uci + * + * Copyright (C) 2015 Hua Shao + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License version 2.1 + * as published by the Free Software Foundation + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. +]] + + +local shuci = {} + +function shuci.decode(path) + function file_exists(name) + if not name then return false end + local f = io.open(name,"r") + if f then io.close(f) return true else return false end + end + local function linebreaker(str) + local i,_ = string.find(str, "([^%s])") + if not i then return nil end + if string.find(str, "config%s+%w+") then + local i,j,k,v = string.find(str, "config%s+([%w-_]+)%s*['\"]*([^%s\'\"]*)") + return "section", k, v + elseif string.find(str, "option%s+%w+") then + local i,j,k,v = string.find(str, "option%s+([%w-_]+)%s*['\"]([^'\"]+)['\"]") + if not k or not v then + i,j,k,v = string.find(str, "option%s+([%w-_]+)%s*['\"]*([^%s\'\"]*)") + end + return "option", k, v + elseif string.find(str, "list%s+%w+") then + local i,j,k,v = string.find(str, "list%s+([%w-_]+)%s*['\"]([^'\"]+)['\"]") + if not k or not v then + i,j,k,v = string.find(str, "list%s+([%w-_]+)%s*['\"]*([^%s\'\"]*)") + end + return "list", k, v + else + print("invalid line!", str) + return nil + end + end + + if not file_exists(path) then + return + end + + local _sect_ = nil + local t = {} + for line in io.lines(path) do + local _type, _name, _value = linebreaker(line) + if _type == "section" then + if not t[_name] then t[_name] = {} end + -- be careful of anonymous sections + if not _value or _value == "" then _value = #t[_name]+1 end + t[_name][_value] = {} + _sect_ = t[_name][_value] + end + if _type == "option" then + if _name and _value then + _sect_[_name] = _value + end + end + if _type == "list" and _name and _value then + local idx + if not _sect_[_name] then + _sect_[_name] = {} + _sect_[_name][1] = _value + else + idx = #_sect_[_name] + _sect_[_name][idx+1] = _value + end + end + end + + return t +end + + +function shuci.encode(t, path) + local dump = io.write + if path then + local fp = io.open(path, "a+") + dump = function(str) fp:write(str) end + end + for _sect_type,_ in pairs(t) do + for _name,_sect in pairs(t[_sect_type]) do + dump(string.format("config\t%s\t'%s'\n", _sect_type, _name)) + for k,v in pairs(_sect) do + if type(v) == "table" then + for _,vv in ipairs(v) do + dump(string.format("\tlist\t%s\t'%s'\n",k,vv)) + end + elseif type(v) == "string" and k ~= ".name" then + dump(string.format("\toption\t%s\t'%s'\n",k,v)) + elseif type(v) == "number" and k ~= ".name" then + dump(string.format("\toption\t%s\t'%s'\n",k,tonumber(v))) + end + end + dump("\n") + end + end +end + + +function shuci.dump(t, indent) + if not indent then indent = 0 end + for k,v in pairs(t) do + if type(v) == "table" then + print(string.rep(" ",indent)..k..":") + shuci.dump(v, indent+4) + else + print(string.rep(" ",indent)..k..":"..v) + end + end +end + +return shuci diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/shuci.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/shuci.luac new file mode 100644 index 000000000000..762dcc1d79af Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/shuci.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket.lua new file mode 100644 index 000000000000..d1c0b1649245 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket.lua @@ -0,0 +1,149 @@ +----------------------------------------------------------------------------- +-- LuaSocket helper module +-- Author: Diego Nehab +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local string = require("string") +local math = require("math") +local socket = require("socket.core") + +local _M = socket + +----------------------------------------------------------------------------- +-- Exported auxiliar functions +----------------------------------------------------------------------------- +function _M.connect4(address, port, laddress, lport) + return socket.connect(address, port, laddress, lport, "inet") +end + +function _M.connect6(address, port, laddress, lport) + return socket.connect(address, port, laddress, lport, "inet6") +end + +function _M.bind(host, port, backlog) + if host == "*" then host = "0.0.0.0" end + local addrinfo, err = socket.dns.getaddrinfo(host); + if not addrinfo then return nil, err end + local sock, res + err = "no info on address" + for i, alt in base.ipairs(addrinfo) do + if alt.family == "inet" then + sock, err = socket.tcp4() + else + sock, err = socket.tcp6() + end + if not sock then return nil, err end + sock:setoption("reuseaddr", true) + res, err = sock:bind(alt.addr, port) + if not res then + sock:close() + else + res, err = sock:listen(backlog) + if not res then + sock:close() + else + return sock + end + end + end + return nil, err +end + +_M.try = _M.newtry() + +function _M.choose(table) + return function(name, opt1, opt2) + if base.type(name) ~= "string" then + name, opt1, opt2 = "default", name, opt1 + end + local f = table[name or "nil"] + if not f then base.error("unknown key (".. base.tostring(name) ..")", 3) + else return f(opt1, opt2) end + end +end + +----------------------------------------------------------------------------- +-- Socket sources and sinks, conforming to LTN12 +----------------------------------------------------------------------------- +-- create namespaces inside LuaSocket namespace +local sourcet, sinkt = {}, {} +_M.sourcet = sourcet +_M.sinkt = sinkt + +_M.BLOCKSIZE = 2048 + +sinkt["close-when-done"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if not chunk then + sock:close() + return 1 + else return sock:send(chunk) end + end + }) +end + +sinkt["keep-open"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if chunk then return sock:send(chunk) + else return 1 end + end + }) +end + +sinkt["default"] = sinkt["keep-open"] + +_M.sink = _M.choose(sinkt) + +sourcet["by-length"] = function(sock, length) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if length <= 0 then return nil end + local size = math.min(socket.BLOCKSIZE, length) + local chunk, err = sock:receive(size) + if err then return nil, err end + length = length - string.len(chunk) + return chunk + end + }) +end + +sourcet["until-closed"] = function(sock) + local done + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + if done then return nil end + local chunk, err, partial = sock:receive(socket.BLOCKSIZE) + if not err then return chunk + elseif err == "closed" then + sock:close() + done = 1 + return partial + else return nil, err end + end + }) +end + + +sourcet["default"] = sourcet["until-closed"] + +_M.source = _M.choose(sourcet) + +return _M diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket.luac new file mode 100644 index 000000000000..50cc6d427015 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/core.so b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/core.so new file mode 120000 index 000000000000..219b03b32ec5 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/core.so @@ -0,0 +1 @@ +../socket-3.0-rc1.so \ No newline at end of file diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/ftp.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/ftp.lua new file mode 100644 index 000000000000..fd66fc463c0b --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/ftp.lua @@ -0,0 +1,329 @@ +----------------------------------------------------------------------------- +-- FTP support for the Lua language +-- LuaSocket toolkit. +-- Author: Diego Nehab +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local table = require("table") +local string = require("string") +local math = require("math") +local socket = require("socket") +local url = require("socket.url") +local tp = require("socket.tp") +local ltn12 = require("ltn12") +socket.ftp = {} +local _M = socket.ftp +----------------------------------------------------------------------------- +-- Program constants +----------------------------------------------------------------------------- +-- timeout in seconds before the program gives up on a connection +_M.TIMEOUT = 60 +-- default port for ftp service +local PORT = 21 +-- this is the default anonymous password. used when no password is +-- provided in url. should be changed to your e-mail. +_M.USER = "ftp" +_M.PASSWORD = "test@example.invalid" + +----------------------------------------------------------------------------- +-- Low level FTP API +----------------------------------------------------------------------------- +local metat = { __index = {} } + +function _M.open(server, port, create) + local tp = socket.try(tp.connect(server, port or PORT, _M.TIMEOUT, create)) + local f = base.setmetatable({ tp = tp }, metat) + -- make sure everything gets closed in an exception + f.try = socket.newtry(function() f:close() end) + return f +end + +function metat.__index:portconnect() + self.try(self.server:settimeout(_M.TIMEOUT)) + self.data = self.try(self.server:accept()) + self.try(self.data:settimeout(_M.TIMEOUT)) +end + +function metat.__index:pasvconnect() + self.data = self.try(socket.tcp()) + self.try(self.data:settimeout(_M.TIMEOUT)) + self.try(self.data:connect(self.pasvt.address, self.pasvt.port)) +end + +function metat.__index:login(user, password) + self.try(self.tp:command("user", user or _M.USER)) + local code, reply = self.try(self.tp:check{"2..", 331}) + if code == 331 then + self.try(self.tp:command("pass", password or _M.PASSWORD)) + self.try(self.tp:check("2..")) + end + return 1 +end + +function metat.__index:pasv() + self.try(self.tp:command("pasv")) + local code, reply = self.try(self.tp:check("2..")) + local pattern = "(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)%D(%d+)" + local a, b, c, d, p1, p2 = socket.skip(2, string.find(reply, pattern)) + self.try(a and b and c and d and p1 and p2, reply) + self.pasvt = { + address = string.format("%d.%d.%d.%d", a, b, c, d), + port = p1*256 + p2 + } + if self.server then + self.server:close() + self.server = nil + end + return self.pasvt.address, self.pasvt.port +end + +function metat.__index:epsv() + self.try(self.tp:command("epsv")) + local code, reply = self.try(self.tp:check("229")) + local pattern = "%((.)(.-)%1(.-)%1(.-)%1%)" + local d, prt, address, port = string.match(reply, pattern) + self.try(port, "invalid epsv response") + self.pasvt = { + address = self.tp:getpeername(), + port = port + } + if self.server then + self.server:close() + self.server = nil + end + return self.pasvt.address, self.pasvt.port +end + + +function metat.__index:port(address, port) + self.pasvt = nil + if not address then + address, port = self.try(self.tp:getsockname()) + self.server = self.try(socket.bind(address, 0)) + address, port = self.try(self.server:getsockname()) + self.try(self.server:settimeout(_M.TIMEOUT)) + end + local pl = math.mod(port, 256) + local ph = (port - pl)/256 + local arg = string.gsub(string.format("%s,%d,%d", address, ph, pl), "%.", ",") + self.try(self.tp:command("port", arg)) + self.try(self.tp:check("2..")) + return 1 +end + +function metat.__index:eprt(family, address, port) + self.pasvt = nil + if not address then + address, port = self.try(self.tp:getsockname()) + self.server = self.try(socket.bind(address, 0)) + address, port = self.try(self.server:getsockname()) + self.try(self.server:settimeout(_M.TIMEOUT)) + end + local arg = string.format("|%s|%s|%d|", family, address, port) + self.try(self.tp:command("eprt", arg)) + self.try(self.tp:check("2..")) + return 1 +end + + +function metat.__index:send(sendt) + self.try(self.pasvt or self.server, "need port or pasv first") + -- if there is a pasvt table, we already sent a PASV command + -- we just get the data connection into self.data + if self.pasvt then self:pasvconnect() end + -- get the transfer argument and command + local argument = sendt.argument or + url.unescape(string.gsub(sendt.path or "", "^[/\\]", "")) + if argument == "" then argument = nil end + local command = sendt.command or "stor" + -- send the transfer command and check the reply + self.try(self.tp:command(command, argument)) + local code, reply = self.try(self.tp:check{"2..", "1.."}) + -- if there is not a pasvt table, then there is a server + -- and we already sent a PORT command + if not self.pasvt then self:portconnect() end + -- get the sink, source and step for the transfer + local step = sendt.step or ltn12.pump.step + local readt = { self.tp } + local checkstep = function(src, snk) + -- check status in control connection while downloading + local readyt = socket.select(readt, nil, 0) + if readyt[tp] then code = self.try(self.tp:check("2..")) end + return step(src, snk) + end + local sink = socket.sink("close-when-done", self.data) + -- transfer all data and check error + self.try(ltn12.pump.all(sendt.source, sink, checkstep)) + if string.find(code, "1..") then self.try(self.tp:check("2..")) end + -- done with data connection + self.data:close() + -- find out how many bytes were sent + local sent = socket.skip(1, self.data:getstats()) + self.data = nil + return sent +end + +function metat.__index:receive(recvt) + self.try(self.pasvt or self.server, "need port or pasv first") + if self.pasvt then self:pasvconnect() end + local argument = recvt.argument or + url.unescape(string.gsub(recvt.path or "", "^[/\\]", "")) + if argument == "" then argument = nil end + local command = recvt.command or "retr" + self.try(self.tp:command(command, argument)) + local code,reply = self.try(self.tp:check{"1..", "2.."}) + if (code >= 200) and (code <= 299) then + recvt.sink(reply) + return 1 + end + if not self.pasvt then self:portconnect() end + local source = socket.source("until-closed", self.data) + local step = recvt.step or ltn12.pump.step + self.try(ltn12.pump.all(source, recvt.sink, step)) + if string.find(code, "1..") then self.try(self.tp:check("2..")) end + self.data:close() + self.data = nil + return 1 +end + +function metat.__index:cwd(dir) + self.try(self.tp:command("cwd", dir)) + self.try(self.tp:check(250)) + return 1 +end + +function metat.__index:type(type) + self.try(self.tp:command("type", type)) + self.try(self.tp:check(200)) + return 1 +end + +function metat.__index:greet() + local code = self.try(self.tp:check{"1..", "2.."}) + if string.find(code, "1..") then self.try(self.tp:check("2..")) end + return 1 +end + +function metat.__index:quit() + self.try(self.tp:command("quit")) + self.try(self.tp:check("2..")) + return 1 +end + +function metat.__index:close() + if self.data then self.data:close() end + if self.server then self.server:close() end + return self.tp:close() +end + +----------------------------------------------------------------------------- +-- High level FTP API +----------------------------------------------------------------------------- +local function override(t) + if t.url then + local u = url.parse(t.url) + for i,v in base.pairs(t) do + u[i] = v + end + return u + else return t end +end + +local function tput(putt) + putt = override(putt) + socket.try(putt.host, "missing hostname") + local f = _M.open(putt.host, putt.port, putt.create) + f:greet() + f:login(putt.user, putt.password) + if putt.type then f:type(putt.type) end + f:epsv() + local sent = f:send(putt) + f:quit() + f:close() + return sent +end + +local default = { + path = "/", + scheme = "ftp" +} + +local function genericform(u) + local t = socket.try(url.parse(u, default)) + socket.try(t.scheme == "ftp", "wrong scheme '" .. t.scheme .. "'") + socket.try(t.host, "missing hostname") + local pat = "^type=(.)$" + if t.params then + t.type = socket.skip(2, string.find(t.params, pat)) + socket.try(t.type == "a" or t.type == "i", + "invalid type '" .. t.type .. "'") + end + return t +end + +_M.genericform = genericform + +local function sput(u, body) + local putt = genericform(u) + putt.source = ltn12.source.string(body) + return tput(putt) +end + +_M.put = socket.protect(function(putt, body) + if base.type(putt) == "string" then return sput(putt, body) + else return tput(putt) end +end) + +local function tget(gett) + gett = override(gett) + socket.try(gett.host, "missing hostname") + local f = _M.open(gett.host, gett.port, gett.create) + f:greet() + f:login(gett.user, gett.password) + if gett.type then f:type(gett.type) end + f:epsv() + f:receive(gett) + f:quit() + return f:close() +end + +local function sget(u) + local gett = genericform(u) + local t = {} + gett.sink = ltn12.sink.table(t) + tget(gett) + return table.concat(t) +end + +_M.command = socket.protect(function(cmdt) + cmdt = override(cmdt) + socket.try(cmdt.host, "missing hostname") + socket.try(cmdt.command, "missing command") + local f = _M.open(cmdt.host, cmdt.port, cmdt.create) + f:greet() + f:login(cmdt.user, cmdt.password) + if type(cmdt.command) == "table" then + local argument = cmdt.argument or {} + local check = cmdt.check or {} + for i,cmd in ipairs(cmdt.command) do + f.try(f.tp:command(cmd, argument[i])) + if check[i] then f.try(f.tp:check(check[i])) end + end + else + f.try(f.tp:command(cmdt.command, cmdt.argument)) + if cmdt.check then f.try(f.tp:check(cmdt.check)) end + end + f:quit() + return f:close() +end) + +_M.get = socket.protect(function(gett) + if base.type(gett) == "string" then return sget(gett) + else return tget(gett) end +end) + +return _M diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/ftp.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/ftp.luac new file mode 100644 index 000000000000..53635f259cad Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/ftp.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/headers.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/headers.lua new file mode 100644 index 000000000000..1eb8223b9ddf --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/headers.lua @@ -0,0 +1,104 @@ +----------------------------------------------------------------------------- +-- Canonic header field capitalization +-- LuaSocket toolkit. +-- Author: Diego Nehab +----------------------------------------------------------------------------- +local socket = require("socket") +socket.headers = {} +local _M = socket.headers + +_M.canonic = { + ["accept"] = "Accept", + ["accept-charset"] = "Accept-Charset", + ["accept-encoding"] = "Accept-Encoding", + ["accept-language"] = "Accept-Language", + ["accept-ranges"] = "Accept-Ranges", + ["action"] = "Action", + ["alternate-recipient"] = "Alternate-Recipient", + ["age"] = "Age", + ["allow"] = "Allow", + ["arrival-date"] = "Arrival-Date", + ["authorization"] = "Authorization", + ["bcc"] = "Bcc", + ["cache-control"] = "Cache-Control", + ["cc"] = "Cc", + ["comments"] = "Comments", + ["connection"] = "Connection", + ["content-description"] = "Content-Description", + ["content-disposition"] = "Content-Disposition", + ["content-encoding"] = "Content-Encoding", + ["content-id"] = "Content-ID", + ["content-language"] = "Content-Language", + ["content-length"] = "Content-Length", + ["content-location"] = "Content-Location", + ["content-md5"] = "Content-MD5", + ["content-range"] = "Content-Range", + ["content-transfer-encoding"] = "Content-Transfer-Encoding", + ["content-type"] = "Content-Type", + ["cookie"] = "Cookie", + ["date"] = "Date", + ["diagnostic-code"] = "Diagnostic-Code", + ["dsn-gateway"] = "DSN-Gateway", + ["etag"] = "ETag", + ["expect"] = "Expect", + ["expires"] = "Expires", + ["final-log-id"] = "Final-Log-ID", + ["final-recipient"] = "Final-Recipient", + ["from"] = "From", + ["host"] = "Host", + ["if-match"] = "If-Match", + ["if-modified-since"] = "If-Modified-Since", + ["if-none-match"] = "If-None-Match", + ["if-range"] = "If-Range", + ["if-unmodified-since"] = "If-Unmodified-Since", + ["in-reply-to"] = "In-Reply-To", + ["keywords"] = "Keywords", + ["last-attempt-date"] = "Last-Attempt-Date", + ["last-modified"] = "Last-Modified", + ["location"] = "Location", + ["max-forwards"] = "Max-Forwards", + ["message-id"] = "Message-ID", + ["mime-version"] = "MIME-Version", + ["original-envelope-id"] = "Original-Envelope-ID", + ["original-recipient"] = "Original-Recipient", + ["pragma"] = "Pragma", + ["proxy-authenticate"] = "Proxy-Authenticate", + ["proxy-authorization"] = "Proxy-Authorization", + ["range"] = "Range", + ["received"] = "Received", + ["received-from-mta"] = "Received-From-MTA", + ["references"] = "References", + ["referer"] = "Referer", + ["remote-mta"] = "Remote-MTA", + ["reply-to"] = "Reply-To", + ["reporting-mta"] = "Reporting-MTA", + ["resent-bcc"] = "Resent-Bcc", + ["resent-cc"] = "Resent-Cc", + ["resent-date"] = "Resent-Date", + ["resent-from"] = "Resent-From", + ["resent-message-id"] = "Resent-Message-ID", + ["resent-reply-to"] = "Resent-Reply-To", + ["resent-sender"] = "Resent-Sender", + ["resent-to"] = "Resent-To", + ["retry-after"] = "Retry-After", + ["return-path"] = "Return-Path", + ["sender"] = "Sender", + ["server"] = "Server", + ["smtp-remote-recipient"] = "SMTP-Remote-Recipient", + ["status"] = "Status", + ["subject"] = "Subject", + ["te"] = "TE", + ["to"] = "To", + ["trailer"] = "Trailer", + ["transfer-encoding"] = "Transfer-Encoding", + ["upgrade"] = "Upgrade", + ["user-agent"] = "User-Agent", + ["vary"] = "Vary", + ["via"] = "Via", + ["warning"] = "Warning", + ["will-retry-until"] = "Will-Retry-Until", + ["www-authenticate"] = "WWW-Authenticate", + ["x-mailer"] = "X-Mailer", +} + +return _M \ No newline at end of file diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/headers.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/headers.luac new file mode 100644 index 000000000000..9603b35d555e Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/headers.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/http.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/http.lua new file mode 100644 index 000000000000..6a3416e0a4a2 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/http.lua @@ -0,0 +1,420 @@ +----------------------------------------------------------------------------- +-- HTTP/1.1 client support for the Lua language. +-- LuaSocket toolkit. +-- Author: Diego Nehab +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +------------------------------------------------------------------------------- +local socket = require("socket") +local url = require("socket.url") +local ltn12 = require("ltn12") +local mime = require("mime") +local string = require("string") +local headers = require("socket.headers") +local base = _G +local table = require("table") +socket.http = {} +local _M = socket.http + +----------------------------------------------------------------------------- +-- Program constants +----------------------------------------------------------------------------- +-- connection timeout in seconds +_M.TIMEOUT = 60 +-- user agent field sent in request +_M.USERAGENT = socket._VERSION + +-- supported schemes and their particulars +local SCHEMES = { + http = { + port = 80 + , create = function(t) + return socket.tcp end } + , https = { + port = 443 + , create = function(t) + local https = assert( + require("ssl.https"), 'LuaSocket: LuaSec not found') + local tcp = assert( + https.tcp, 'LuaSocket: Function tcp() not available from LuaSec') + return tcp(t) end }} + +-- default scheme and port for document retrieval +local SCHEME = 'http' +local PORT = SCHEMES[SCHEME].port +----------------------------------------------------------------------------- +-- Reads MIME headers from a connection, unfolding where needed +----------------------------------------------------------------------------- +local function receiveheaders(sock, headers) + local line, name, value, err + headers = headers or {} + -- get first line + line, err = sock:receive() + if err then return nil, err end + -- headers go until a blank line is found + while line ~= "" do + -- get field-name and value + name, value = socket.skip(2, string.find(line, "^(.-):%s*(.*)")) + if not (name and value) then return nil, "malformed reponse headers" end + name = string.lower(name) + -- get next line (value might be folded) + line, err = sock:receive() + if err then return nil, err end + -- unfold any folded values + while string.find(line, "^%s") do + value = value .. line + line = sock:receive() + if err then return nil, err end + end + -- save pair in table + if headers[name] then headers[name] = headers[name] .. ", " .. value + else headers[name] = value end + end + return headers +end + +----------------------------------------------------------------------------- +-- Extra sources and sinks +----------------------------------------------------------------------------- +socket.sourcet["http-chunked"] = function(sock, headers) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function() + -- get chunk size, skip extention + local line, err = sock:receive() + if err then return nil, err end + local size = base.tonumber(string.gsub(line, ";.*", ""), 16) + if not size then return nil, "invalid chunk size" end + -- was it the last chunk? + if size > 0 then + -- if not, get chunk and skip terminating CRLF + local chunk, err, part = sock:receive(size) + if chunk then sock:receive() end + return chunk, err + else + -- if it was, read trailers into headers table + headers, err = receiveheaders(sock, headers) + if not headers then return nil, err end + end + end + }) +end + +socket.sinkt["http-chunked"] = function(sock) + return base.setmetatable({ + getfd = function() return sock:getfd() end, + dirty = function() return sock:dirty() end + }, { + __call = function(self, chunk, err) + if not chunk then return sock:send("0\r\n\r\n") end + local size = string.format("%X\r\n", string.len(chunk)) + return sock:send(size .. chunk .. "\r\n") + end + }) +end + +----------------------------------------------------------------------------- +-- Low level HTTP API +----------------------------------------------------------------------------- +local metat = { __index = {} } + +function _M.open(host, port, create) + -- create socket with user connect function, or with default + local c = socket.try(create()) + local h = base.setmetatable({ c = c }, metat) + -- create finalized try + h.try = socket.newtry(function() h:close() end) + -- set timeout before connecting + h.try(c:settimeout(_M.TIMEOUT)) + h.try(c:connect(host, port)) + -- here everything worked + return h +end + +function metat.__index:sendrequestline(method, uri) + local reqline = string.format("%s %s HTTP/1.1\r\n", method or "GET", uri) + return self.try(self.c:send(reqline)) +end + +function metat.__index:sendheaders(tosend) + local canonic = headers.canonic + local h = "\r\n" + for f, v in base.pairs(tosend) do + h = (canonic[f] or f) .. ": " .. v .. "\r\n" .. h + end + self.try(self.c:send(h)) + return 1 +end + +function metat.__index:sendbody(headers, source, step) + source = source or ltn12.source.empty() + step = step or ltn12.pump.step + -- if we don't know the size in advance, send chunked and hope for the best + local mode = "http-chunked" + if headers["content-length"] then mode = "keep-open" end + return self.try(ltn12.pump.all(source, socket.sink(mode, self.c), step)) +end + +function metat.__index:receivestatusline() + local status,ec = self.try(self.c:receive(5)) + -- identify HTTP/0.9 responses, which do not contain a status line + -- this is just a heuristic, but is what the RFC recommends + if status ~= "HTTP/" then + if ec == "timeout" then + return 408 + end + return nil, status + end + -- otherwise proceed reading a status line + status = self.try(self.c:receive("*l", status)) + local code = socket.skip(2, string.find(status, "HTTP/%d*%.%d* (%d%d%d)")) + return self.try(base.tonumber(code), status) +end + +function metat.__index:receiveheaders() + return self.try(receiveheaders(self.c)) +end + +function metat.__index:receivebody(headers, sink, step) + sink = sink or ltn12.sink.null() + step = step or ltn12.pump.step + local length = base.tonumber(headers["content-length"]) + local t = headers["transfer-encoding"] -- shortcut + local mode = "default" -- connection close + if t and t ~= "identity" then mode = "http-chunked" + elseif base.tonumber(headers["content-length"]) then mode = "by-length" end + return self.try(ltn12.pump.all(socket.source(mode, self.c, length), + sink, step)) +end + +function metat.__index:receive09body(status, sink, step) + local source = ltn12.source.rewind(socket.source("until-closed", self.c)) + source(status) + return self.try(ltn12.pump.all(source, sink, step)) +end + +function metat.__index:close() + return self.c:close() +end + +----------------------------------------------------------------------------- +-- High level HTTP API +----------------------------------------------------------------------------- +local function adjusturi(reqt) + local u = reqt + -- if there is a proxy, we need the full url. otherwise, just a part. + if not reqt.proxy and not _M.PROXY then + u = { + path = socket.try(reqt.path, "invalid path 'nil'"), + params = reqt.params, + query = reqt.query, + fragment = reqt.fragment + } + end + return url.build(u) +end + +local function adjustproxy(reqt) + local proxy = reqt.proxy or _M.PROXY + if proxy then + proxy = url.parse(proxy) + return proxy.host, proxy.port or 3128 + else + return reqt.host, reqt.port + end +end + +local function adjustheaders(reqt) + -- default headers + local host = reqt.host + local port = tostring(reqt.port) + if port ~= tostring(SCHEMES[reqt.scheme].port) then + host = host .. ':' .. port end + local lower = { + ["user-agent"] = _M.USERAGENT, + ["host"] = host, + ["connection"] = "close, TE", + ["te"] = "trailers" + } + -- if we have authentication information, pass it along + if reqt.user and reqt.password then + lower["authorization"] = + "Basic " .. (mime.b64(reqt.user .. ":" .. + url.unescape(reqt.password))) + end + -- if we have proxy authentication information, pass it along + local proxy = reqt.proxy or _M.PROXY + if proxy then + proxy = url.parse(proxy) + if proxy.user and proxy.password then + lower["proxy-authorization"] = + "Basic " .. (mime.b64(proxy.user .. ":" .. proxy.password)) + end + end + -- override with user headers + for i,v in base.pairs(reqt.headers or lower) do + lower[string.lower(i)] = v + end + return lower +end + +-- default url parts +local default = { + path ="/" + , scheme = "http" +} + +local function adjustrequest(reqt) + -- parse url if provided + local nreqt = reqt.url and url.parse(reqt.url, default) or {} + -- explicit components override url + for i,v in base.pairs(reqt) do nreqt[i] = v end + -- default to scheme particulars + local schemedefs, host, port, method + = SCHEMES[nreqt.scheme], nreqt.host, nreqt.port, nreqt.method + if not nreqt.create then nreqt.create = schemedefs.create(nreqt) end + if not (port and port ~= '') then nreqt.port = schemedefs.port end + if not (method and method ~= '') then nreqt.method = 'GET' end + if not (host and host ~= "") then + socket.try(nil, "invalid host '" .. base.tostring(nreqt.host) .. "'") + end + -- compute uri if user hasn't overriden + nreqt.uri = reqt.uri or adjusturi(nreqt) + -- adjust headers in request + nreqt.headers = adjustheaders(nreqt) + -- ajust host and port if there is a proxy + nreqt.host, nreqt.port = adjustproxy(nreqt) + return nreqt +end + +local function shouldredirect(reqt, code, headers) + local location = headers.location + if not location then return false end + location = string.gsub(location, "%s", "") + if location == "" then return false end + local scheme = url.parse(location).scheme + if scheme and (not SCHEMES[scheme]) then return false end + -- avoid https downgrades + if ('https' == reqt.scheme) and ('https' ~= scheme) then return false end + return (reqt.redirect ~= false) and + (code == 301 or code == 302 or code == 303 or code == 307) and + (not reqt.method or reqt.method == "GET" or reqt.method == "HEAD") + and ((false == reqt.maxredirects) + or ((reqt.nredirects or 0) + < (reqt.maxredirects or 5))) +end + +local function shouldreceivebody(reqt, code) + if reqt.method == "HEAD" then return nil end + if code == 204 or code == 304 then return nil end + if code >= 100 and code < 200 then return nil end + return 1 +end + +-- forward declarations +local trequest, tredirect + +--[[local]] function tredirect(reqt, location) + -- the RFC says the redirect URL has to be absolute, but some + -- servers do not respect that + local newurl = url.absolute(reqt.url, location) + -- if switching schemes, reset port and create function + if url.parse(newurl).scheme ~= reqt.scheme then + reqt.port = nil + reqt.create = nil end + -- make new request + local result, code, headers, status = trequest { + url = newurl, + source = reqt.source, + sink = reqt.sink, + headers = reqt.headers, + proxy = reqt.proxy, + maxredirects = reqt.maxredirects, + nredirects = (reqt.nredirects or 0) + 1, + create = reqt.create + } + -- pass location header back as a hint we redirected + headers = headers or {} + headers.location = headers.location or location + return result, code, headers, status +end + +--[[local]] function trequest(reqt) + -- we loop until we get what we want, or + -- until we are sure there is no way to get it + local nreqt = adjustrequest(reqt) + local h = _M.open(nreqt.host, nreqt.port, nreqt.create) + -- send request line and headers + h:sendrequestline(nreqt.method, nreqt.uri) + h:sendheaders(nreqt.headers) + -- if there is a body, send it + if nreqt.source then + h:sendbody(nreqt.headers, nreqt.source, nreqt.step) + end + local code, status = h:receivestatusline() + -- if it is an HTTP/0.9 server, simply get the body and we are done + if not code then + h:receive09body(status, nreqt.sink, nreqt.step) + return 1, 200 + elseif code == 408 then + return 1, code + end + local headers + -- ignore any 100-continue messages + while code == 100 do + headers = h:receiveheaders() + code, status = h:receivestatusline() + end + headers = h:receiveheaders() + -- at this point we should have a honest reply from the server + -- we can't redirect if we already used the source, so we report the error + if shouldredirect(nreqt, code, headers) and not nreqt.source then + h:close() + return tredirect(reqt, headers.location) + end + -- here we are finally done + if shouldreceivebody(nreqt, code) then + h:receivebody(headers, nreqt.sink, nreqt.step) + end + h:close() + return 1, code, headers, status +end + +-- turns an url and a body into a generic request +local function genericform(u, b) + local t = {} + local reqt = { + url = u, + sink = ltn12.sink.table(t), + target = t + } + if b then + reqt.source = ltn12.source.string(b) + reqt.headers = { + ["content-length"] = string.len(b), + ["content-type"] = "application/x-www-form-urlencoded" + } + reqt.method = "POST" + end + return reqt +end + +_M.genericform = genericform + +local function srequest(u, b) + local reqt = genericform(u, b) + local _, code, headers, status = trequest(reqt) + return table.concat(reqt.target), code, headers, status +end + +_M.request = socket.protect(function(reqt, body) + if base.type(reqt) == "string" then return srequest(reqt, body) + else return trequest(reqt) end +end) + +_M.schemes = SCHEMES +return _M diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/http.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/http.luac new file mode 100644 index 000000000000..79fee13be695 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/http.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/smtp.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/smtp.lua new file mode 100644 index 000000000000..b113d0067731 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/smtp.lua @@ -0,0 +1,256 @@ +----------------------------------------------------------------------------- +-- SMTP client support for the Lua language. +-- LuaSocket toolkit. +-- Author: Diego Nehab +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local coroutine = require("coroutine") +local string = require("string") +local math = require("math") +local os = require("os") +local socket = require("socket") +local tp = require("socket.tp") +local ltn12 = require("ltn12") +local headers = require("socket.headers") +local mime = require("mime") + +socket.smtp = {} +local _M = socket.smtp + +----------------------------------------------------------------------------- +-- Program constants +----------------------------------------------------------------------------- +-- timeout for connection +_M.TIMEOUT = 60 +-- default server used to send e-mails +_M.SERVER = "localhost" +-- default port +_M.PORT = 25 +-- domain used in HELO command and default sendmail +-- If we are under a CGI, try to get from environment +_M.DOMAIN = os.getenv("SERVER_NAME") or "localhost" +-- default time zone (means we don't know) +_M.ZONE = "-0000" + +--------------------------------------------------------------------------- +-- Low level SMTP API +----------------------------------------------------------------------------- +local metat = { __index = {} } + +function metat.__index:greet(domain) + self.try(self.tp:check("2..")) + self.try(self.tp:command("EHLO", domain or _M.DOMAIN)) + return socket.skip(1, self.try(self.tp:check("2.."))) +end + +function metat.__index:mail(from) + self.try(self.tp:command("MAIL", "FROM:" .. from)) + return self.try(self.tp:check("2..")) +end + +function metat.__index:rcpt(to) + self.try(self.tp:command("RCPT", "TO:" .. to)) + return self.try(self.tp:check("2..")) +end + +function metat.__index:data(src, step) + self.try(self.tp:command("DATA")) + self.try(self.tp:check("3..")) + self.try(self.tp:source(src, step)) + self.try(self.tp:send("\r\n.\r\n")) + return self.try(self.tp:check("2..")) +end + +function metat.__index:quit() + self.try(self.tp:command("QUIT")) + return self.try(self.tp:check("2..")) +end + +function metat.__index:close() + return self.tp:close() +end + +function metat.__index:login(user, password) + self.try(self.tp:command("AUTH", "LOGIN")) + self.try(self.tp:check("3..")) + self.try(self.tp:send(mime.b64(user) .. "\r\n")) + self.try(self.tp:check("3..")) + self.try(self.tp:send(mime.b64(password) .. "\r\n")) + return self.try(self.tp:check("2..")) +end + +function metat.__index:plain(user, password) + local auth = "PLAIN " .. mime.b64("\0" .. user .. "\0" .. password) + self.try(self.tp:command("AUTH", auth)) + return self.try(self.tp:check("2..")) +end + +function metat.__index:auth(user, password, ext) + if not user or not password then return 1 end + if string.find(ext, "AUTH[^\n]+LOGIN") then + return self:login(user, password) + elseif string.find(ext, "AUTH[^\n]+PLAIN") then + return self:plain(user, password) + else + self.try(nil, "authentication not supported") + end +end + +-- send message or throw an exception +function metat.__index:send(mailt) + self:mail(mailt.from) + if base.type(mailt.rcpt) == "table" then + for i,v in base.ipairs(mailt.rcpt) do + self:rcpt(v) + end + else + self:rcpt(mailt.rcpt) + end + self:data(ltn12.source.chain(mailt.source, mime.stuff()), mailt.step) +end + +function _M.open(server, port, create) + local tp = socket.try(tp.connect(server or _M.SERVER, port or _M.PORT, + _M.TIMEOUT, create)) + local s = base.setmetatable({tp = tp}, metat) + -- make sure tp is closed if we get an exception + s.try = socket.newtry(function() + s:close() + end) + return s +end + +-- convert headers to lowercase +local function lower_headers(headers) + local lower = {} + for i,v in base.pairs(headers or lower) do + lower[string.lower(i)] = v + end + return lower +end + +--------------------------------------------------------------------------- +-- Multipart message source +----------------------------------------------------------------------------- +-- returns a hopefully unique mime boundary +local seqno = 0 +local function newboundary() + seqno = seqno + 1 + return string.format('%s%05d==%05u', os.date('%d%m%Y%H%M%S'), + math.random(0, 99999), seqno) +end + +-- send_message forward declaration +local send_message + +-- yield the headers all at once, it's faster +local function send_headers(tosend) + local canonic = headers.canonic + local h = "\r\n" + for f,v in base.pairs(tosend) do + h = (canonic[f] or f) .. ': ' .. v .. "\r\n" .. h + end + coroutine.yield(h) +end + +-- yield multipart message body from a multipart message table +local function send_multipart(mesgt) + -- make sure we have our boundary and send headers + local bd = newboundary() + local headers = lower_headers(mesgt.headers or {}) + headers['content-type'] = headers['content-type'] or 'multipart/mixed' + headers['content-type'] = headers['content-type'] .. + '; boundary="' .. bd .. '"' + send_headers(headers) + -- send preamble + if mesgt.body.preamble then + coroutine.yield(mesgt.body.preamble) + coroutine.yield("\r\n") + end + -- send each part separated by a boundary + for i, m in base.ipairs(mesgt.body) do + coroutine.yield("\r\n--" .. bd .. "\r\n") + send_message(m) + end + -- send last boundary + coroutine.yield("\r\n--" .. bd .. "--\r\n\r\n") + -- send epilogue + if mesgt.body.epilogue then + coroutine.yield(mesgt.body.epilogue) + coroutine.yield("\r\n") + end +end + +-- yield message body from a source +local function send_source(mesgt) + -- make sure we have a content-type + local headers = lower_headers(mesgt.headers or {}) + headers['content-type'] = headers['content-type'] or + 'text/plain; charset="iso-8859-1"' + send_headers(headers) + -- send body from source + while true do + local chunk, err = mesgt.body() + if err then coroutine.yield(nil, err) + elseif chunk then coroutine.yield(chunk) + else break end + end +end + +-- yield message body from a string +local function send_string(mesgt) + -- make sure we have a content-type + local headers = lower_headers(mesgt.headers or {}) + headers['content-type'] = headers['content-type'] or + 'text/plain; charset="iso-8859-1"' + send_headers(headers) + -- send body from string + coroutine.yield(mesgt.body) +end + +-- message source +function send_message(mesgt) + if base.type(mesgt.body) == "table" then send_multipart(mesgt) + elseif base.type(mesgt.body) == "function" then send_source(mesgt) + else send_string(mesgt) end +end + +-- set defaul headers +local function adjust_headers(mesgt) + local lower = lower_headers(mesgt.headers) + lower["date"] = lower["date"] or + os.date("!%a, %d %b %Y %H:%M:%S ") .. (mesgt.zone or _M.ZONE) + lower["x-mailer"] = lower["x-mailer"] or socket._VERSION + -- this can't be overriden + lower["mime-version"] = "1.0" + return lower +end + +function _M.message(mesgt) + mesgt.headers = adjust_headers(mesgt) + -- create and return message source + local co = coroutine.create(function() send_message(mesgt) end) + return function() + local ret, a, b = coroutine.resume(co) + if ret then return a, b + else return nil, a end + end +end + +--------------------------------------------------------------------------- +-- High level SMTP API +----------------------------------------------------------------------------- +_M.send = socket.protect(function(mailt) + local s = _M.open(mailt.server, mailt.port, mailt.create) + local ext = s:greet(mailt.domain) + s:auth(mailt.user, mailt.password, ext) + s:send(mailt) + s:quit() + return s:close() +end) + +return _M \ No newline at end of file diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/smtp.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/smtp.luac new file mode 100644 index 000000000000..c09dbc133780 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/smtp.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/tp.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/tp.lua new file mode 100644 index 000000000000..b8ebc56d16a2 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/tp.lua @@ -0,0 +1,134 @@ +----------------------------------------------------------------------------- +-- Unified SMTP/FTP subsystem +-- LuaSocket toolkit. +-- Author: Diego Nehab +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module and import dependencies +----------------------------------------------------------------------------- +local base = _G +local string = require("string") +local socket = require("socket") +local ltn12 = require("ltn12") + +socket.tp = {} +local _M = socket.tp + +----------------------------------------------------------------------------- +-- Program constants +----------------------------------------------------------------------------- +_M.TIMEOUT = 60 + +----------------------------------------------------------------------------- +-- Implementation +----------------------------------------------------------------------------- +-- gets server reply (works for SMTP and FTP) +local function get_reply(c) + local code, current, sep + local line, err = c:receive() + local reply = line + if err then return nil, err end + code, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)")) + if not code then return nil, "invalid server reply" end + if sep == "-" then -- reply is multiline + repeat + line, err = c:receive() + if err then return nil, err end + current, sep = socket.skip(2, string.find(line, "^(%d%d%d)(.?)")) + reply = reply .. "\n" .. line + -- reply ends with same code + until code == current and sep == " " + end + return code, reply +end + +-- metatable for sock object +local metat = { __index = {} } + +function metat.__index:getpeername() + return self.c:getpeername() +end + +function metat.__index:getsockname() + return self.c:getpeername() +end + +function metat.__index:check(ok) + local code, reply = get_reply(self.c) + if not code then return nil, reply end + if base.type(ok) ~= "function" then + if base.type(ok) == "table" then + for i, v in base.ipairs(ok) do + if string.find(code, v) then + return base.tonumber(code), reply + end + end + return nil, reply + else + if string.find(code, ok) then return base.tonumber(code), reply + else return nil, reply end + end + else return ok(base.tonumber(code), reply) end +end + +function metat.__index:command(cmd, arg) + cmd = string.upper(cmd) + if arg then + return self.c:send(cmd .. " " .. arg.. "\r\n") + else + return self.c:send(cmd .. "\r\n") + end +end + +function metat.__index:sink(snk, pat) + local chunk, err = self.c:receive(pat) + return snk(chunk, err) +end + +function metat.__index:send(data) + return self.c:send(data) +end + +function metat.__index:receive(pat) + return self.c:receive(pat) +end + +function metat.__index:getfd() + return self.c:getfd() +end + +function metat.__index:dirty() + return self.c:dirty() +end + +function metat.__index:getcontrol() + return self.c +end + +function metat.__index:source(source, step) + local sink = socket.sink("keep-open", self.c) + local ret, err = ltn12.pump.all(source, sink, step or ltn12.pump.step) + return ret, err +end + +-- closes the underlying c +function metat.__index:close() + self.c:close() + return 1 +end + +-- connect with server and return c object +function _M.connect(host, port, timeout, create) + local c, e = (create or socket.tcp)() + if not c then return nil, e end + c:settimeout(timeout or _M.TIMEOUT) + local r, e = c:connect(host, port) + if not r then + c:close() + return nil, e + end + return base.setmetatable({c = c}, metat) +end + +return _M diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/tp.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/tp.luac new file mode 100644 index 000000000000..5375c373c9c0 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/tp.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/url.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/url.lua new file mode 100644 index 000000000000..0a3a80a67289 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/url.lua @@ -0,0 +1,331 @@ +----------------------------------------------------------------------------- +-- URI parsing, composition and relative URL resolution +-- LuaSocket toolkit. +-- Author: Diego Nehab +----------------------------------------------------------------------------- + +----------------------------------------------------------------------------- +-- Declare module +----------------------------------------------------------------------------- +local string = require("string") +local base = _G +local table = require("table") +local socket = require("socket") + +socket.url = {} +local _M = socket.url + +----------------------------------------------------------------------------- +-- Module version +----------------------------------------------------------------------------- +_M._VERSION = "URL 1.0.3" + +----------------------------------------------------------------------------- +-- Encodes a string into its escaped hexadecimal representation +-- Input +-- s: binary string to be encoded +-- Returns +-- escaped representation of string binary +----------------------------------------------------------------------------- +function _M.escape(s) + return (string.gsub(s, "([^A-Za-z0-9_])", function(c) + return string.format("%%%02x", string.byte(c)) + end)) +end + +----------------------------------------------------------------------------- +-- Protects a path segment, to prevent it from interfering with the +-- url parsing. +-- Input +-- s: binary string to be encoded +-- Returns +-- escaped representation of string binary +----------------------------------------------------------------------------- +local function make_set(t) + local s = {} + for i,v in base.ipairs(t) do + s[t[i]] = 1 + end + return s +end + +-- these are allowed within a path segment, along with alphanum +-- other characters must be escaped +local segment_set = make_set { + "-", "_", ".", "!", "~", "*", "'", "(", + ")", ":", "@", "&", "=", "+", "$", ",", +} + +local function protect_segment(s) + return string.gsub(s, "([^A-Za-z0-9_])", function (c) + if segment_set[c] then return c + else return string.format("%%%02X", string.byte(c)) end + end) +end + +----------------------------------------------------------------------------- +-- Unencodes a escaped hexadecimal string into its binary representation +-- Input +-- s: escaped hexadecimal string to be unencoded +-- Returns +-- unescaped binary representation of escaped hexadecimal binary +----------------------------------------------------------------------------- +function _M.unescape(s) + return (string.gsub(s, "%%(%x%x)", function(hex) + return string.char(base.tonumber(hex, 16)) + end)) +end + +----------------------------------------------------------------------------- +-- Removes '..' and '.' components appropriately from a path. +-- Input +-- path +-- Returns +-- dot-normalized path +local function remove_dot_components(path) + local marker = string.char(1) + repeat + local was = path + path = path:gsub('//', '/'..marker..'/', 1) + until path == was + repeat + local was = path + path = path:gsub('/%./', '/', 1) + until path == was + repeat + local was = path + path = path:gsub('[^/]+/%.%./([^/]+)', '%1', 1) + until path == was + path = path:gsub('[^/]+/%.%./*$', '') + path = path:gsub('/%.%.$', '/') + path = path:gsub('/%.$', '/') + path = path:gsub('^/%.%./', '/') + path = path:gsub(marker, '') + return path +end + +----------------------------------------------------------------------------- +-- Builds a path from a base path and a relative path +-- Input +-- base_path +-- relative_path +-- Returns +-- corresponding absolute path +----------------------------------------------------------------------------- +local function absolute_path(base_path, relative_path) + if string.sub(relative_path, 1, 1) == "/" then + return remove_dot_components(relative_path) end + base_path = base_path:gsub("[^/]*$", "") + if not base_path:find'/$' then base_path = base_path .. '/' end + local path = base_path .. relative_path + path = remove_dot_components(path) + return path +end + +----------------------------------------------------------------------------- +-- Parses a url and returns a table with all its parts according to RFC 2396 +-- The following grammar describes the names given to the URL parts +-- ::= :///;?# +-- ::= @: +-- ::= [:] +-- :: = {/} +-- Input +-- url: uniform resource locator of request +-- default: table with default values for each field +-- Returns +-- table with the following fields, where RFC naming conventions have +-- been preserved: +-- scheme, authority, userinfo, user, password, host, port, +-- path, params, query, fragment +-- Obs: +-- the leading '/' in {/} is considered part of +----------------------------------------------------------------------------- +function _M.parse(url, default) + -- initialize default parameters + local parsed = {} + for i,v in base.pairs(default or parsed) do parsed[i] = v end + -- empty url is parsed to nil + if not url or url == "" then return nil, "invalid url" end + -- remove whitespace + -- url = string.gsub(url, "%s", "") + -- get scheme + url = string.gsub(url, "^([%w][%w%+%-%.]*)%:", + function(s) parsed.scheme = s; return "" end) + -- get authority + url = string.gsub(url, "^//([^/]*)", function(n) + parsed.authority = n + return "" + end) + -- get fragment + url = string.gsub(url, "#(.*)$", function(f) + parsed.fragment = f + return "" + end) + -- get query string + url = string.gsub(url, "%?(.*)", function(q) + parsed.query = q + return "" + end) + -- get params + url = string.gsub(url, "%;(.*)", function(p) + parsed.params = p + return "" + end) + -- path is whatever was left + if url ~= "" then parsed.path = url end + local authority = parsed.authority + if not authority then return parsed end + authority = string.gsub(authority,"^([^@]*)@", + function(u) parsed.userinfo = u; return "" end) + authority = string.gsub(authority, ":([^:%]]*)$", + function(p) parsed.port = p; return "" end) + if authority ~= "" then + -- IPv6? + parsed.host = string.match(authority, "^%[(.+)%]$") or authority + end + local userinfo = parsed.userinfo + if not userinfo then return parsed end + userinfo = string.gsub(userinfo, ":([^:]*)$", + function(p) parsed.password = p; return "" end) + parsed.user = userinfo + return parsed +end + +----------------------------------------------------------------------------- +-- Rebuilds a parsed URL from its components. +-- Components are protected if any reserved or unallowed characters are found +-- Input +-- parsed: parsed URL, as returned by parse +-- Returns +-- a stringing with the corresponding URL +----------------------------------------------------------------------------- +function _M.build(parsed) + --local ppath = _M.parse_path(parsed.path or "") + --local url = _M.build_path(ppath) + local url = parsed.path or "" + if parsed.params then url = url .. ";" .. parsed.params end + if parsed.query then url = url .. "?" .. parsed.query end + local authority = parsed.authority + if parsed.host then + authority = parsed.host + if string.find(authority, ":") then -- IPv6? + authority = "[" .. authority .. "]" + end + if parsed.port then authority = authority .. ":" .. base.tostring(parsed.port) end + local userinfo = parsed.userinfo + if parsed.user then + userinfo = parsed.user + if parsed.password then + userinfo = userinfo .. ":" .. parsed.password + end + end + if userinfo then authority = userinfo .. "@" .. authority end + end + if authority then url = "//" .. authority .. url end + if parsed.scheme then url = parsed.scheme .. ":" .. url end + if parsed.fragment then url = url .. "#" .. parsed.fragment end + -- url = string.gsub(url, "%s", "") + return url +end + +----------------------------------------------------------------------------- +-- Builds a absolute URL from a base and a relative URL according to RFC 2396 +-- Input +-- base_url +-- relative_url +-- Returns +-- corresponding absolute url +----------------------------------------------------------------------------- +function _M.absolute(base_url, relative_url) + local base_parsed + if base.type(base_url) == "table" then + base_parsed = base_url + base_url = _M.build(base_parsed) + else + base_parsed = _M.parse(base_url) + end + local result + local relative_parsed = _M.parse(relative_url) + if not base_parsed then + result = relative_url + elseif not relative_parsed then + result = base_url + elseif relative_parsed.scheme then + result = relative_url + else + relative_parsed.scheme = base_parsed.scheme + if not relative_parsed.authority then + relative_parsed.authority = base_parsed.authority + if not relative_parsed.path then + relative_parsed.path = base_parsed.path + if not relative_parsed.params then + relative_parsed.params = base_parsed.params + if not relative_parsed.query then + relative_parsed.query = base_parsed.query + end + end + else + relative_parsed.path = absolute_path(base_parsed.path or "", + relative_parsed.path) + end + end + result = _M.build(relative_parsed) + end + return remove_dot_components(result) +end + +----------------------------------------------------------------------------- +-- Breaks a path into its segments, unescaping the segments +-- Input +-- path +-- Returns +-- segment: a table with one entry per segment +----------------------------------------------------------------------------- +function _M.parse_path(path) + local parsed = {} + path = path or "" + --path = string.gsub(path, "%s", "") + string.gsub(path, "([^/]+)", function (s) table.insert(parsed, s) end) + for i = 1, #parsed do + parsed[i] = _M.unescape(parsed[i]) + end + if string.sub(path, 1, 1) == "/" then parsed.is_absolute = 1 end + if string.sub(path, -1, -1) == "/" then parsed.is_directory = 1 end + return parsed +end + +----------------------------------------------------------------------------- +-- Builds a path component from its segments, escaping protected characters. +-- Input +-- parsed: path segments +-- unsafe: if true, segments are not protected before path is built +-- Returns +-- path: corresponding path stringing +----------------------------------------------------------------------------- +function _M.build_path(parsed, unsafe) + local path = "" + local n = #parsed + if unsafe then + for i = 1, n-1 do + path = path .. parsed[i] + path = path .. "/" + end + if n > 0 then + path = path .. parsed[n] + if parsed.is_directory then path = path .. "/" end + end + else + for i = 1, n-1 do + path = path .. protect_segment(parsed[i]) + path = path .. "/" + end + if n > 0 then + path = path .. protect_segment(parsed[n]) + if parsed.is_directory then path = path .. "/" end + end + end + if parsed.is_absolute then path = "/" .. path end + return path +end + +return _M diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/url.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/url.luac new file mode 100644 index 000000000000..4f80f481622b Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/socket/url.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/wps_action.lua b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/wps_action.lua new file mode 100755 index 000000000000..110163833f99 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/wps_action.lua @@ -0,0 +1,26 @@ +#!/usr/bin/env lua +local dat_parser = require("l1dat_parser") +ifname = arg[1] + +if (ifname ~= "") then + + zone_name = dat_parser.l1_ifname_to_zone(ifname) + if (zone_name ~= "") then + -- 2860 interface check + if (string.find(zone_name,"dev1") ~= nil) then + os.execute("killall -SIGXFSZ nvram_daemon") + end + + -- RTDEV interface check + if (string.find(zone_name,"dev2") ~= nil) then + os.execute("killall -SIGWINCH nvram_daemon") + end + + -- wifi3 interface check + if (string.find(zone_name,"dev3") ~= nil) then + os.execute("killall -SIGPWR nvram_daemon") + end + else + print "unable to find zone name" + end +end \ No newline at end of file diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/wps_action.luac b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/wps_action.luac new file mode 100644 index 000000000000..5b826868fa2c Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/openwrt-derived-firmware-lua/usr/lib/lua/wps_action.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/SAMPLE-MANIFEST.md b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/SAMPLE-MANIFEST.md new file mode 100644 index 000000000000..a09756150a6b --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/SAMPLE-MANIFEST.md @@ -0,0 +1,20 @@ +# Lua2Cpg Rules, Sanitizer, And Report Samples + +These committed bytecode samples support the `RulesSanitizerReportSmokeTest` +reviewer smoke. They are consumed entirely inside the `lua2cpg` test resources +and provide focused Lua 5.1 bytecode coverage for source/sink matching, +sanitizer classification, report construction, and negative taint boundaries. + +| Fixture | Fixture role | Capability reason | Consuming reviewer command | +| --- | --- | --- | --- | +| `bc-taint-minimal-path/input.luac` | Focused committed fixture | Minimal source-to-sink path for rule, endpoint, report, and report-boundary smoke coverage. | `JAVA_TOOL_OPTIONS='-Dsbt.watch.mode=polling -Dsbt.io.jdktimestamps=true' sbt 'lua2cpg/testOnly io.joern.lua2cpg.RulesSanitizerReportSmokeTest'` | +| `d16-rf-formvalue-os-execute-chain/input.luac` | Focused committed fixture | Final-segment `*.formvalue` source and `*.execute` sink positive with a constructed report. | `JAVA_TOOL_OPTIONS='-Dsbt.watch.mode=polling -Dsbt.io.jdktimestamps=true' sbt 'lua2cpg/testOnly io.joern.lua2cpg.RulesSanitizerReportSmokeTest'` | +| `d16-rf-submit-dpp-uri-execute/input.luac` | Focused committed fixture | Independent same-module formvalue-to-execute report positive. | `JAVA_TOOL_OPTIONS='-Dsbt.watch.mode=polling -Dsbt.io.jdktimestamps=true' sbt 'lua2cpg/testOnly io.joern.lua2cpg.RulesSanitizerReportSmokeTest'` | +| `d16-rf-webcmd-cross-module-popen/controller.luac` | Focused committed fixture | Cross-module source side for final-segment source/sink and report construction. | `JAVA_TOOL_OPTIONS='-Dsbt.watch.mode=polling -Dsbt.io.jdktimestamps=true' sbt 'lua2cpg/testOnly io.joern.lua2cpg.RulesSanitizerReportSmokeTest'` | +| `d16-rf-webcmd-cross-module-popen/mtkwifi.luac` | Focused committed fixture | Cross-module `*.popen` sink side for final-segment sink and report construction. | `JAVA_TOOL_OPTIONS='-Dsbt.watch.mode=polling -Dsbt.io.jdktimestamps=true' sbt 'lua2cpg/testOnly io.joern.lua2cpg.RulesSanitizerReportSmokeTest'` | +| `d24-sanitizer-suppresses-report/input.luac` | Focused committed fixture | On-chain sanitizer positive; emits sanitized classification and suppresses true-positive vulnerability reporting. | `JAVA_TOOL_OPTIONS='-Dsbt.watch.mode=polling -Dsbt.io.jdktimestamps=true' sbt 'lua2cpg/testOnly io.joern.lua2cpg.RulesSanitizerReportSmokeTest'` | +| `d24-rules-overmatch-constant-sink-negative/input.luac` | Focused committed fixture | Rejects `formvaluex`, `executex`, and fixed-string sink arguments. | `JAVA_TOOL_OPTIONS='-Dsbt.watch.mode=polling -Dsbt.io.jdktimestamps=true' sbt 'lua2cpg/testOnly io.joern.lua2cpg.RulesSanitizerReportSmokeTest'` | +| `d24-sanitizer-same-suffix-off-chain-negative/input.luac` | Focused committed fixture | Same-suffix sanitizer call not on the path remains `not-sanitized` and does not suppress the report. | `JAVA_TOOL_OPTIONS='-Dsbt.watch.mode=polling -Dsbt.io.jdktimestamps=true' sbt 'lua2cpg/testOnly io.joern.lua2cpg.RulesSanitizerReportSmokeTest'` | +| `d24-report-no-report-without-path-negative/input.luac` | Focused committed fixture | Endpoint-only source/sink evidence does not create a vulnerability report. | `JAVA_TOOL_OPTIONS='-Dsbt.watch.mode=polling -Dsbt.io.jdktimestamps=true' sbt 'lua2cpg/testOnly io.joern.lua2cpg.RulesSanitizerReportSmokeTest'` | +| `bc-kill-overwrite/input.luac` | Focused committed fixture | Killed taint path negative boundary; no vulnerability report should be emitted. | `JAVA_TOOL_OPTIONS='-Dsbt.watch.mode=polling -Dsbt.io.jdktimestamps=true' sbt 'lua2cpg/testOnly io.joern.lua2cpg.RulesSanitizerReportSmokeTest'` | +| `bc-branch-negative/input.luac` | Focused committed fixture | Branch-negative no-path boundary; no vulnerability report should be emitted. | `JAVA_TOOL_OPTIONS='-Dsbt.watch.mode=polling -Dsbt.io.jdktimestamps=true' sbt 'lua2cpg/testOnly io.joern.lua2cpg.RulesSanitizerReportSmokeTest'` | diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/bc-branch-negative/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/bc-branch-negative/input.luac new file mode 100644 index 000000000000..29c4dfe7ad77 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/bc-branch-negative/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/bc-kill-overwrite/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/bc-kill-overwrite/input.luac new file mode 100644 index 000000000000..7ecd2a1f96b9 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/bc-kill-overwrite/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/bc-taint-minimal-path/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/bc-taint-minimal-path/input.luac new file mode 100644 index 000000000000..39a8a1b6f4e9 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/bc-taint-minimal-path/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/bc-taint-minimal-path/source.lua b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/bc-taint-minimal-path/source.lua new file mode 100644 index 000000000000..5442625f707f --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/bc-taint-minimal-path/source.lua @@ -0,0 +1,2 @@ +local value = luci.http.formvalue("value") +os.execute(value) diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d16-rf-formvalue-os-execute-chain/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d16-rf-formvalue-os-execute-chain/input.luac new file mode 100644 index 000000000000..ec523dfb20b4 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d16-rf-formvalue-os-execute-chain/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d16-rf-submit-dpp-uri-execute/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d16-rf-submit-dpp-uri-execute/input.luac new file mode 100644 index 000000000000..b71721903e60 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d16-rf-submit-dpp-uri-execute/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d16-rf-webcmd-cross-module-popen/controller.luac b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d16-rf-webcmd-cross-module-popen/controller.luac new file mode 100644 index 000000000000..adc8690d65c8 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d16-rf-webcmd-cross-module-popen/controller.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d16-rf-webcmd-cross-module-popen/mtkwifi.luac b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d16-rf-webcmd-cross-module-popen/mtkwifi.luac new file mode 100644 index 000000000000..34aa80ab8ba7 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d16-rf-webcmd-cross-module-popen/mtkwifi.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-report-no-report-without-path-negative/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-report-no-report-without-path-negative/input.luac new file mode 100644 index 000000000000..947715cff58c Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-report-no-report-without-path-negative/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-rules-overmatch-constant-sink-negative/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-rules-overmatch-constant-sink-negative/input.luac new file mode 100644 index 000000000000..e5b9a5fbff4e Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-rules-overmatch-constant-sink-negative/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-sanitizer-same-suffix-off-chain-negative/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-sanitizer-same-suffix-off-chain-negative/input.luac new file mode 100644 index 000000000000..980a55dda0a5 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-sanitizer-same-suffix-off-chain-negative/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-sanitizer-same-suffix-off-chain-negative/source.lua b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-sanitizer-same-suffix-off-chain-negative/source.lua new file mode 100644 index 000000000000..27fde19d9d98 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-sanitizer-same-suffix-off-chain-negative/source.lua @@ -0,0 +1,16 @@ +luci = { http = {} } + +function luci.http.formvalue(name) + return name +end + +os = { + execute = function(cmd) + return cmd + end +} + +local tainted = luci.http.formvalue("cmd") +local clean = "fixed" +tonumber(clean) +os.execute(tainted) diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-sanitizer-suppresses-report/input.luac b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-sanitizer-suppresses-report/input.luac new file mode 100644 index 000000000000..8a4ef64f1fb0 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-sanitizer-suppresses-report/input.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-sanitizer-suppresses-report/source.lua b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-sanitizer-suppresses-report/source.lua new file mode 100644 index 000000000000..00fb754f1934 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/d24-sanitizer-suppresses-report/source.lua @@ -0,0 +1,15 @@ +luci = { http = {} } + +function luci.http.formvalue(name) + return name +end + +os = { + execute = function(cmd) + return cmd + end +} + +local tainted = luci.http.formvalue("cmd") +local sanitized = tonumber(tainted) +os.execute(sanitized) diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-generic/nested/controller.lua b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-generic/nested/controller.lua new file mode 100644 index 000000000000..1782c7d88f13 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-generic/nested/controller.lua @@ -0,0 +1,5 @@ +local formatter = require("formatter") +local command = luci.http.formvalue("cmd") +local safe_command = formatter._cmdformat(command) + +return os.execute(safe_command) diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-generic/nested/controller.luac b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-generic/nested/controller.luac new file mode 100644 index 000000000000..15a00943e5a2 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-generic/nested/controller.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-generic/vendor/formatter.lua b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-generic/vendor/formatter.lua new file mode 100644 index 000000000000..6352b21f0819 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-generic/vendor/formatter.lua @@ -0,0 +1,7 @@ +local M = {} + +function M._cmdformat(value) + return value +end + +return M diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-generic/vendor/formatter.luac b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-generic/vendor/formatter.luac new file mode 100644 index 000000000000..5fffef7aad4c Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-generic/vendor/formatter.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-lookalike/controller.lua b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-lookalike/controller.lua new file mode 100644 index 000000000000..c6053fa6fa8c --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-lookalike/controller.lua @@ -0,0 +1,5 @@ +local formatter = unknown_provider() +local command = luci.http.formvalue("cmd") +local unchanged = formatter._cmdformat(command) + +return os.execute(unchanged) diff --git a/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-lookalike/controller.luac b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-lookalike/controller.luac new file mode 100644 index 000000000000..e9261e243941 Binary files /dev/null and b/joern-cli/frontends/lua2cpg/src/test/resources/rules-sanitizer-report/sanitizer-rule-lookalike/controller.luac differ diff --git a/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/BytecodeModelSmokeTest.scala b/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/BytecodeModelSmokeTest.scala new file mode 100644 index 000000000000..5247cc185296 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/BytecodeModelSmokeTest.scala @@ -0,0 +1,77 @@ +package io.joern.lua2cpg + +import io.shiftleft.codepropertygraph.cpgloading.CpgLoader +import io.shiftleft.semanticcpg.language.* +import io.shiftleft.semanticcpg.language.types.structure.FileTraversal +import io.shiftleft.semanticcpg.utils.FileUtil +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import java.nio.file.Paths + +class BytecodeModelSmokeTest extends AnyWordSpec with Matchers { + + "Lua2Cpg" should { + "emit bytecode model nodes that survive CPG reopen" in { + val resourceRoot = Paths.get(getClass.getClassLoader.getResource("bytecode-model").toURI).getParent + + FileUtil.usingTemporaryDirectory("lua2cpg-bytecode-model-smoke") { tmpDir => + val outputPath = tmpDir.resolve("bytecode-model.cpg.bin").toString + val cpg = new Lua2Cpg() + .createCpg(Config().withInputPath(resourceRoot.toString).withOutputPath(outputPath)) + .get + cpg.close() + + val reopened = CpgLoader.load(outputPath) + try { + reopened.file.nameNot(FileTraversal.UNKNOWN).name.sorted.l should contain allOf ( + "bytecode-model/bc-prototype-params/input.luac", + "bytecode-model/bc-constants-call/input.luac", + "bytecode-model/bc-stripped-metadata/input.luac" + ) + + // Expected rows are anchored in the committed bytecode fixtures for + // bc-prototype-params, bc-constants-call, bc-stripped-metadata, and bc-malformed-diagnostic. + val methodFullNames = reopened.method.fullName.sorted.l + methodFullNames should contain allOf ( + "lua:bytecode-model/bc-prototype-params/input.luac:root", + "lua:bytecode-model/bc-prototype-params/input.luac:root.0", + "lua:bytecode-model/bc-constants-call/input.luac:root", + "lua:bytecode-model/bc-constants-call/input.luac:root.0", + "lua:bytecode-model/bc-stripped-metadata/input.luac:root", + "lua:bytecode-model/bc-stripped-metadata/input.luac:root.0" + ) + + reopened.method + .fullNameExact("lua:bytecode-model/bc-prototype-params/input.luac:root.0") + .parameter + .indexGt(0) + .index + .sorted + .l shouldBe List(1, 2) + + val literalCodes = reopened.literal.code.l + literalCodes should contain allOf ("alpha", "7") + + val diagnostics = reopened.typeDecl + .name("lua-bytecode-diagnostic") + .fullName + .l + diagnostics should contain allOf ( + "lua:bytecode-model/bc-malformed-diagnostic/not-lua-bytecode.luac:diagnostic:not-lua-bytecode", + "lua:bytecode-model/bc-malformed-diagnostic/truncated.luac:diagnostic:truncated-bytecode", + "lua:bytecode-model/bc-malformed-diagnostic/unsupported-version.luac:diagnostic:unsupported-bytecode-version", + "lua:bytecode-model/bc-malformed-diagnostic/unsupported-profile.luac:diagnostic:unsupported-bytecode-profile", + "lua:bytecode-model/bc-malformed-diagnostic/malformed-constant.luac:diagnostic:malformed-constant", + "lua:bytecode-model/bc-stripped-metadata/input.luac:diagnostic:metadata-unavailable" + ) + diagnostics.exists(_.contains("not-lua-bytecode.bin")) shouldBe false + + methodFullNames.filter(_.startsWith("lua:bytecode-model/bc-malformed-diagnostic/")) shouldBe Nil + } finally { + reopened.close() + } + } + } + } +} diff --git a/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/InterproceduralModuleTaintSmokeTest.scala b/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/InterproceduralModuleTaintSmokeTest.scala new file mode 100644 index 000000000000..834cae4bf589 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/InterproceduralModuleTaintSmokeTest.scala @@ -0,0 +1,157 @@ +package io.joern.lua2cpg + +import io.shiftleft.codepropertygraph.cpgloading.CpgLoader +import io.shiftleft.codepropertygraph.generated.EdgeTypes +import io.shiftleft.semanticcpg.language.* +import io.shiftleft.semanticcpg.utils.FileUtil +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import java.nio.file.Paths + +class InterproceduralModuleTaintSmokeTest extends AnyWordSpec with Matchers { + + "Lua2Cpg" should { + "emit interprocedural module and taint markers that survive CPG reopen" in { + val resourceRoot = Paths.get(getClass.getClassLoader.getResource("interprocedural-module-taint").toURI) + + FileUtil.usingTemporaryDirectory("lua2cpg-interprocedural-module-taint-smoke") { tmpDir => + val outputPath = tmpDir.resolve("interprocedural-module-taint.cpg.bin").toString + val cpg = new Lua2Cpg() + .createCpg(Config().withInputPath(resourceRoot.toString).withOutputPath(outputPath)) + .get + cpg.close() + + val reopened = CpgLoader.load(outputPath) + try { + markerCodes(reopened, "lua.interproc.arg_flow") should contain( + "d16-rf-interprocedural-formvalue-execute/input.luac:root@pc18:r4 -> d16-rf-interprocedural-formvalue-execute/input.luac:root.3:r0" + ) + markerCodes(reopened, "lua.interproc.arg_flow") should contain( + "d24-interproc-unresolved-callee-negative/input.luac:root@pc8:r6 -> d24-interproc-unresolved-callee-negative/input.luac:root.2:r1" + ) + markerCodes(reopened, "lua.interproc.return_flow") should contain( + "d16-rf-interprocedural-formvalue-execute/input.luac::root.2@pc4:r0 -> d16-rf-interprocedural-formvalue-execute/input.luac:root@pc15:r2" + ) + markerCodes(reopened, "lua.interproc.arg_flow") + .exists(code => + code.contains("bridge-flow-generic/controller.luac") && code.contains("bridge.luac:root.0:r0") + ) shouldBe true + markerCodes(reopened, "lua.interproc.return_flow") + .exists(code => + code.contains("bridge-flow-generic/bridge.luac::root.0") && code.contains("controller.luac") + ) shouldBe true + markerCodes(reopened, "lua.module.resolution") should contain( + "d16-rf-webcmd-cross-module-popen/controller.luac require mtkwifi -> matched:d16-rf-webcmd-cross-module-popen/mtkwifi.luac" + ) + markerCodes(reopened, "lua.module.resolution") should contain allOf ( + "module-resolution-generic/controller.luac require foo -> matched:module-resolution-generic/a/foo.luac", + "module-resolution-generic/controller.luac require luci.util -> matched:module-resolution-generic/vendor/luci/util.luac" + ) + markerCodes(reopened, "lua.module.return_table") should contain( + "d24-module-return-table-field-call/returnlib.luac::run -> root.0" + ) + markerCodes(reopened, "lua.module.field_call_target") should contain allOf ( + "d24-module-return-table-field-call/controller.luac:root.0@pc10 -> d24-module-return-table-field-call/returnlib.luac::root.0", + "d24-module-return-table-field-call/controller.luac:root.1@pc10 -> d24-module-return-table-field-call/returnlib.luac::root.0" + ) + markerCodes(reopened, "lua.calltarget.cross_boundary") should contain( + "d16-rf-webcmd-cross-module-popen/controller.luac:root.1@pc8 -> d16-rf-webcmd-cross-module-popen/mtkwifi.luac::root.1" + ) + markerCodes(reopened, "lua.taint.path") should contain( + "bc-taint-minimal-path/input.luac:root@pc4:r0 -> bc-taint-minimal-path/input.luac:root@pc8:r2 via bc-taint-minimal-path/input.luac:root@pc4:r0;bc-taint-minimal-path/input.luac:root@pc7:r0;bc-taint-minimal-path/input.luac:root@pc7:r2;bc-taint-minimal-path/input.luac:root@pc8:r2" + ) + val genericBridgePaths = markerCodes(reopened, "lua.taint.path").filter(_.contains("bridge-flow-generic")) + withClue(s"generic bridge paths: ${genericBridgePaths.mkString(", ")}") { + genericBridgePaths.exists(_.contains("bridge-flow-generic/bridge.luac")) shouldBe true + } + val callContextNegativePaths = markerCodes(reopened, "lua.taint.path") + .filter(_.contains("call-context-negative")) + withClue(s"call-context negative paths: ${callContextNegativePaths.mkString(", ")}") { + callContextNegativePaths shouldBe empty + } + val tableFieldNegativePaths = markerCodes(reopened, "lua.taint.path") + .filter(_.contains("table-field-provenance-negative")) + withClue(s"table field provenance negative paths: ${tableFieldNegativePaths.mkString(", ")}") { + tableFieldNegativePaths shouldBe empty + } + val tableFieldPositivePaths = markerCodes(reopened, "lua.taint.path") + .filter(_.contains("table-field-provenance-positive")) + withClue(s"table field provenance positive paths: ${tableFieldPositivePaths.mkString(", ")}") { + tableFieldPositivePaths should not be empty + } + val nestedBranchOverwritePaths = markerCodes(reopened, "lua.taint.path") + .filter(_.contains("nested-branch-overwrite-negative")) + withClue(s"nested branch overwrite paths: ${nestedBranchOverwritePaths.mkString(", ")}") { + nestedBranchOverwritePaths shouldBe empty + } + val conditionalMergePaths = markerCodes(reopened, "lua.taint.path") + .filter(_.contains("conditional-merge-positive")) + withClue(s"conditional merge paths: ${conditionalMergePaths.mkString(", ")}") { + conditionalMergePaths should not be empty + } + val controlSelectionPaths = markerCodes(reopened, "lua.taint.path") + .filter(_.contains("control-selection-positive")) + withClue(s"control selection paths: ${controlSelectionPaths.mkString(", ")}") { + controlSelectionPaths should not be empty + } + val unrelatedControlPaths = markerCodes(reopened, "lua.taint.path") + .filter(_.contains("control-unrelated-negative")) + withClue(s"unrelated control paths: ${unrelatedControlPaths.mkString(", ")}") { + unrelatedControlPaths shouldBe empty + } + val overwrittenControlPaths = markerCodes(reopened, "lua.taint.path") + .filter(_.contains("control-overwrite-negative")) + withClue(s"overwritten control paths: ${overwrittenControlPaths.mkString(", ")}") { + overwrittenControlPaths shouldBe empty + } + + val unresolvedArgFlows = markerCodes(reopened, "lua.interproc.arg_flow") + .filter(_.contains("d24-interproc-unresolved-callee-negative")) + withClue(s"unresolved arg flows: ${unresolvedArgFlows.mkString(", ")}") { + unresolvedArgFlows.exists(_.contains("root.2@pc2")) shouldBe false + } + val unresolvedReturnFlows = markerCodes(reopened, "lua.interproc.return_flow") + .filter(_.contains("d24-interproc-unresolved-callee-negative")) + withClue(s"unresolved return flows: ${unresolvedReturnFlows.mkString(", ")}") { + unresolvedReturnFlows.exists(_.contains("root.2@pc2")) shouldBe false + } + markerCodes(reopened, "lua.module.resolution") should contain( + "d24-module-ambiguous-unresolved-dynamic-negative/ambiguous.luac require shared.module -> matched:d24-module-ambiguous-unresolved-dynamic-negative/left.luac" + ) + markerCodes(reopened, "lua.calltarget.cross_boundary") + .exists(_.contains("d24-module-missing-field-negative")) shouldBe false + markerCodes(reopened, "lua.taint.path") + .exists(code => code.contains("bc-kill-overwrite") || code.contains("bc-branch-negative")) shouldBe false + + val boundaryCodes = markerCodes(reopened, "lua.e4.boundary") + boundaryCodes should contain allOf ( + "d24-interproc-unresolved-callee-negative/input.luac:root.2@pc2 reason=unresolved-callee", + "d24-module-ambiguous-unresolved-dynamic-negative/missing.luac:require:missing.module reason=unresolved-module", + "d24-module-ambiguous-unresolved-dynamic-negative/controller.luac:require:dynamic reason=dynamic-require", + "d24-module-missing-field-negative/controller.luac:root.0@pc3 reason=missing-export-field" + ) + + val e4NodeCount = reopened.call + .name( + "lua\\.(module\\.resolution|module\\.return_table|module\\.field_call_target|interproc\\.arg_flow|interproc\\.return_flow|calltarget\\.cross_boundary|taint\\.path|e4\\.boundary)" + ) + .size + val e4ReachingDefEdgeCount = reopened.identifier.outE(EdgeTypes.REACHING_DEF).size + val e4TaintPathCount = reopened.call.nameExact("lua.taint.path").size + info(s"e4_node_count=$e4NodeCount") + info(s"e4_reaching_def_edge_count=$e4ReachingDefEdgeCount") + info(s"e4_taint_path_count=$e4TaintPathCount") + e4NodeCount should be > 0 + e4ReachingDefEdgeCount should be > 0 + e4TaintPathCount should be > 0 + } finally { + reopened.close() + } + } + } + } + + private def markerCodes(cpg: io.shiftleft.codepropertygraph.generated.Cpg, name: String): List[String] = + cpg.call.nameExact(name).code.l +} diff --git a/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/IntraproceduralSemanticsSmokeTest.scala b/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/IntraproceduralSemanticsSmokeTest.scala new file mode 100644 index 000000000000..b09fe98d06e0 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/IntraproceduralSemanticsSmokeTest.scala @@ -0,0 +1,204 @@ +package io.joern.lua2cpg + +import io.shiftleft.codepropertygraph.cpgloading.CpgLoader +import io.shiftleft.codepropertygraph.generated.EdgeTypes +import io.shiftleft.codepropertygraph.generated.nodes.{CfgNode, StoredNode} +import io.shiftleft.semanticcpg.language.* +import io.shiftleft.semanticcpg.utils.FileUtil +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import java.nio.file.{Files, Paths} + +class IntraproceduralSemanticsSmokeTest extends AnyWordSpec with Matchers { + + "Lua2Cpg" should { + "emit intraprocedural bytecode semantics that survive CPG reopen" in { + val resourceRoot = Paths.get(getClass.getClassLoader.getResource("intraprocedural-semantics").toURI) + + FileUtil.usingTemporaryDirectory("lua2cpg-intraprocedural-semantics-smoke") { tmpDir => + val outputPath = tmpDir.resolve("intraprocedural-semantics.cpg.bin").toString + val cpg = new Lua2Cpg() + .createCpg(Config().withInputPath(resourceRoot.toString).withOutputPath(outputPath)) + .get + cpg.close() + + val reopened = CpgLoader.load(outputPath) + try { + hasReachingDef(reopened, "bc-kill-overwrite", "root@pc4:r2", "root@pc7:r4") shouldBe true + hasReachingDef(reopened, "d24-defuse-transitive-chain", "root@pc3:r2", "root@pc8:r6") shouldBe true + hasReachingDef(reopened, "local-value-flow-generic", "root.0:r0", "root.0@pc6:r4") shouldBe true + hasReachingDef(reopened, "local-value-flow-generic", "root.2:r0", "root.2@pc6:r3") shouldBe true + + hasReachingDef(reopened, "bc-kill-overwrite", "root@pc3:r2", "root@pc7:r4") shouldBe false + hasReachingDef(reopened, "local-value-flow-generic", "root.1:r0", "root.1@pc6:r3") shouldBe false + hasReachingDef( + reopened, + "d24-defuse-unrelated-register-negative", + "root@pc4:r2", + "root@pc9:r6" + ) shouldBe false + hasReachingDef(reopened, "d24-table-dynamic-key-negative", "root@pc3:r2", "root@pc4:r3") shouldBe false + hasReachingDef(reopened, "d24-table-dynamic-key-negative", "root@pc3:r2", "root@pc5:r4") shouldBe false + hasReachingDef(reopened, "d24-global-dynamic-env-negative", "root@pc3:r1", "root@pc4:r2") shouldBe false + hasReachingDef(reopened, "d24-global-dynamic-env-negative", "root@pc3:r1", "root@pc5:r3") shouldBe false + hasReachingDef(reopened, "d24-upvalue-mutation-negative", "root.0@pc0:r0", "root.0@pc3:r1") shouldBe false + + hasSemanticEdge( + reopened, + "bc-table-global-upvalue", + "table:root.0@pc3:r1:root.0:k0", + "root.0@pc1:r0", + "root.0@pc3:r3" + ) shouldBe true + hasReachingDef(reopened, "bc-table-global-upvalue", "root.0@pc6:r3", "root.0@pc8:r2") shouldBe true + hasSemanticEdge( + reopened, + "bc-table-global-upvalue", + "upvalue:root.0:u0", + "root.0@pc4:r4", + "root.0@pc4:r4" + ) shouldBe true + + reopened.call + .nameExact("lua.calltarget.candidate") + .codeExact("root@pc5 -> root.1") + .nonEmpty shouldBe true + reopened.call + .nameExact("lua.calltarget.unresolved") + .codeExact("root.1@pc2 unresolved=param-derived") + .nonEmpty shouldBe true + reopened.call + .nameExact("lua.calltarget.candidate") + .code(".*source-function-name.*") + .isEmpty shouldBe true + + val actualBoundaries = reopened.call.nameExact("lua.semantic.boundary").code.l.toSet + actualBoundaries.exists(_.startsWith("no-stale-upvalue-reuse-after-setupval:")) shouldBe true + actualBoundaries.exists(_.startsWith("upvalue-mutation-boundary:")) shouldBe true + actualBoundaries.size shouldBe 2 + + val nodeCount = reopened.graph.allNodes.size + val reachingDefCount = reopened.identifier.outE(EdgeTypes.REACHING_DEF).size + (nodeCount > 0) shouldBe true + (reachingDefCount > 0) shouldBe true + info(s"intraprocedural-semantics node_count=$nodeCount reaching_def_edge_count=$reachingDefCount") + } finally { + reopened.close() + } + } + } + + "preserve positive and negative flow semantics under neutral fixture names" in { + val resourceRoot = Paths.get(getClass.getClassLoader.getResource("intraprocedural-semantics").toURI) + + FileUtil.usingTemporaryDirectory("lua2cpg-renamed-semantics-smoke") { tmpDir => + val inputRoot = tmpDir.resolve("input") + val positiveDir = inputRoot.resolve("alpha") + val negativeDir = inputRoot.resolve("beta") + Files.createDirectories(positiveDir) + Files.createDirectories(negativeDir) + Files.copy(resourceRoot.resolve("local-value-flow-generic/input.luac"), positiveDir.resolve("input.luac")) + Files.copy(resourceRoot.resolve("bc-kill-overwrite/input.luac"), negativeDir.resolve("input.luac")) + + val outputPath = tmpDir.resolve("renamed-semantics.cpg.bin").toString + val cpg = new Lua2Cpg() + .createCpg(Config().withInputPath(inputRoot.toString).withOutputPath(outputPath)) + .get + cpg.close() + + val reopened = CpgLoader.load(outputPath) + try { + hasReachingDef(reopened, "alpha", "root.0:r0", "root.0@pc6:r4") shouldBe true + hasReachingDef(reopened, "alpha", "root.1:r0", "root.1@pc6:r3") shouldBe false + hasReachingDef(reopened, "beta", "root@pc4:r2", "root@pc7:r4") shouldBe true + hasReachingDef(reopened, "beta", "root@pc3:r2", "root@pc7:r4") shouldBe false + } finally { + reopened.close() + } + } + } + } + + private def hasReachingDef( + cpg: io.shiftleft.codepropertygraph.generated.Cpg, + fixtureId: String, + sourceCode: String, + sinkCode: String + ): Boolean = + hasSemanticEdge(cpg, fixtureId, sourceCode, sourceCode, sinkCode) || transitiveReachingDef( + cpg, + fixtureId, + sourceCode, + sinkCode + ) + + private def hasSemanticEdge( + cpg: io.shiftleft.codepropertygraph.generated.Cpg, + fixtureId: String, + variable: String, + sourceCode: String, + sinkCode: String + ): Boolean = { + val fixtureNodes = semanticNodesInFixture(cpg, fixtureId) + val sinkIds = fixtureNodes + .filter(_.code == sinkCode) + .map(_.id) + .toSet + + fixtureNodes + .filter(_.code == sourceCode) + .outE(EdgeTypes.REACHING_DEF) + .filter(edge => Option(edge.property).contains(variable)) + .exists { edge => + val sink = edge.dst.asInstanceOf[StoredNode] + sinkIds.contains(sink.id) + } + } + + private def transitiveReachingDef( + cpg: io.shiftleft.codepropertygraph.generated.Cpg, + fixtureId: String, + sourceCode: String, + sinkCode: String + ): Boolean = { + val fixtureNodes = semanticNodesInFixture(cpg, fixtureId) + val fixtureNodeCodes = fixtureNodes + .map(identifier => identifier.id -> identifier.code) + .toMap + + val graph = fixtureNodes + .outE(EdgeTypes.REACHING_DEF) + .flatMap { edge => + val sourceNode = edge.src.asInstanceOf[StoredNode] + val sinkNode = edge.dst.asInstanceOf[StoredNode] + val source = fixtureNodeCodes.get(sourceNode.id) + val sink = fixtureNodeCodes.get(sinkNode.id) + source.zip(sink).headOption + } + .foldLeft(Map.empty[String, Set[String]]) { case (acc, (source, sink)) => + acc.updated(source, acc.getOrElse(source, Set.empty) + sink) + } + val seen = scala.collection.mutable.Set.empty[String] + val work = scala.collection.mutable.Stack(sourceCode) + while (work.nonEmpty) { + val current = work.pop() + if (current == sinkCode) { + return true + } + if (!seen(current)) { + seen += current + graph.getOrElse(current, Set.empty).diff(seen.toSet).foreach(work.push) + } + } + false + } + + private def semanticNodesInFixture( + cpg: io.shiftleft.codepropertygraph.generated.Cpg, + fixtureId: String + ): List[CfgNode] = + cpg.method.filename(s".*$fixtureId/input\\.luac").parameter.l ++ + cpg.method.filename(s".*$fixtureId/input\\.luac").ast.isIdentifier.l + +} diff --git a/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/OpenWrtDerivedFirmwareCorpusSmokeTest.scala b/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/OpenWrtDerivedFirmwareCorpusSmokeTest.scala new file mode 100644 index 000000000000..fbca3ca5691d --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/OpenWrtDerivedFirmwareCorpusSmokeTest.scala @@ -0,0 +1,63 @@ +package io.joern.lua2cpg + +import io.shiftleft.codepropertygraph.cpgloading.CpgLoader +import io.shiftleft.semanticcpg.language.* +import io.shiftleft.semanticcpg.language.types.structure.FileTraversal +import io.shiftleft.semanticcpg.utils.FileUtil +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import java.nio.file.{Files, Paths} +import scala.jdk.CollectionConverters.* + +class OpenWrtDerivedFirmwareCorpusSmokeTest extends AnyWordSpec with Matchers { + + "Lua2Cpg" should { + "analyze the OpenWrt-derived Lua corpus and export decoder evidence" in { + val resourceRoot = + Paths.get(getClass.getClassLoader.getResource("openwrt-derived-firmware-lua/usr/lib/lua").toURI) + + FileUtil.usingTemporaryDirectory("lua2cpg-openwrt-derived-corpus-smoke") { tmpDir => + val outputPath = tmpDir.resolve("openwrt-derived-firmware-lua.cpg.bin").toString + val exportDir = tmpDir.resolve("openwrt-derived-firmware-lua-evidence") + val cpg = new Lua2Cpg() + .createCpg( + Config(realFirmwareOutputDir = Some(exportDir.toString)) + .withInputPath(resourceRoot.toString) + .withOutputPath(outputPath) + ) + .get + cpg.close() + + Files.isRegularFile(Paths.get(outputPath)) shouldBe true + + val reopened = CpgLoader.load(outputPath) + try { + reopened.metaData.language.l shouldBe List("LUA") + reopened.file.nameNot(FileTraversal.UNKNOWN).name.l should contain allOf ( + "luci/http.lua", + "luci/http.luac", + "luci/controller/mtkwifi.lua", + "luci/controller/mtkwifi.luac", + "mtkwifi.lua", + "mtkwifi.luac" + ) + } finally { + reopened.close() + } + + val decoderTotals = ujson.read(Files.readString(exportDir.resolve("decoder-summary.json"))).obj("totals").obj + decoderTotals("input_count").num.toInt shouldBe 42 + decoderTotals("decoded_count").num.toInt shouldBe 42 + decoderTotals("diagnostic_count").num.toInt shouldBe 0 + + val stagingStream = Files.list(exportDir.resolve("staging")) + try { + stagingStream.iterator.asScala.count(Files.isRegularFile(_)) shouldBe 42 + } finally { + stagingStream.close() + } + } + } + } +} diff --git a/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/RealFirmwareEvidenceExportSmokeTest.scala b/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/RealFirmwareEvidenceExportSmokeTest.scala new file mode 100644 index 000000000000..073224dd31e5 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/RealFirmwareEvidenceExportSmokeTest.scala @@ -0,0 +1,238 @@ +package io.joern.lua2cpg + +import io.joern.lua2cpg.bytecode.{ + LuaPairPerformanceProfile, + LuaPathSearchStats, + LuaPerformanceAttribution, + LuaProgramSemantics, + LuaRealFirmwareEvidenceExporter, + LuaTaintPath +} +import io.shiftleft.semanticcpg.utils.FileUtil +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import java.nio.file.{Files, Path, Paths} +import scala.jdk.CollectionConverters.* + +class RealFirmwareEvidenceExportSmokeTest extends AnyWordSpec with Matchers { + + "Lua2Cpg" should { + "export Lua evidence with scoped callsite rows" in { + val resourceRoot = Paths.get(getClass.getClassLoader.getResource("rules-sanitizer-report").toURI) + + FileUtil.usingTemporaryDirectory("lua2cpg-real-firmware-export-smoke") { tmpDir => + val outputPath = tmpDir.resolve("rules-sanitizer-report.cpg.bin").toString + val exportDir = tmpDir.resolve("real-firmware-export") + val cpg = new Lua2Cpg() + .createCpg( + Config(realFirmwareOutputDir = Some(exportDir.toString)) + .withInputPath(resourceRoot.toString) + .withOutputPath(outputPath) + ) + .get + cpg.close() + + Files.isRegularFile(exportDir.resolve("decoder-summary.json")) shouldBe true + Files.isRegularFile(exportDir.resolve("run-summary.json")) shouldBe true + Files.isRegularFile(exportDir.resolve("run-errors.json")) shouldBe true + Files.isRegularFile(exportDir.resolve("path-search-profile.json")) shouldBe true + + val staging = stagingRows(exportDir) + .find(_("relative_path").str.endsWith("d24-sanitizer-suppresses-report/input.luac")) + .getOrElse(fail("missing sanitizer fixture staging evidence")) + + val callRows = staging("call_name_resolution").arr.map(_.obj) + callRows.exists(row => + row("module_path").str.endsWith("d24-sanitizer-suppresses-report/input.luac") && + hasScopedCallsite(row, "root@pc19") && + row("resolved_name").str == "tonumber" + ) shouldBe true + + val pathRows = staging("path_evidence").arr.map(_.obj) + pathRows.exists(row => + row("path_steps").arr.exists(_.str.endsWith("d24-sanitizer-suppresses-report/input.luac::root@pc19:r1")) + ) shouldBe true + } + } + + "reject fixture paths without real endpoints before creating output" in { + val counterNames = Vector( + "source_reachability_check_count", + "source_reachability_accepted_count", + "prototype_unreachable_pair_count", + "source_specific_provenance_pruned_pair_count", + "parameter_position_check_count", + "parameter_position_accepted_count", + "parameter_position_pruned_count", + "path_constructor_check_count", + "path_constructor_accepted_count", + "path_constructor_pruned_count", + "bridge_argument_provenance_candidate_count", + "bridge_candidate_pc_pruned_count", + "bridge_candidate_reachability_pruned_count", + "bridge_local_path_attempt_count", + "bridge_local_path_success_count", + "local_path_search_count", + "distinct_local_path_query_count", + "local_path_cache_hit_count", + "local_path_cache_miss_count", + "local_path_graph_build_count", + "local_path_graph_cache_hit_count", + "local_path_graph_cache_miss_count", + "bridge_path_cache_hit_count", + "bridge_path_cache_miss_count", + "targeted_search_node_visit_count", + "targeted_search_edge_visit_count", + "early_candidate_short_circuit_count", + "taint_path_count", + "report_count" + ) + val counters = counterNames.map(_ -> 0L).toMap ++ Map( + "source_reachability_check_count" -> 1L, + "source_reachability_accepted_count" -> 1L, + "parameter_position_check_count" -> 1L, + "parameter_position_accepted_count" -> 1L, + "path_constructor_check_count" -> 1L, + "path_constructor_accepted_count" -> 1L, + "local_path_search_count" -> 1L, + "distinct_local_path_query_count" -> 1L, + "local_path_cache_miss_count" -> 1L, + "taint_path_count" -> 1L + ) + val sourceRef = "bc-endpoint-contract/input.luac:root@pc1:r0" + val sinkRef = "bc-endpoint-contract/input.luac:root@pc2:r1" + val pair = LuaPairPerformanceProfile( + sourceRef, + sinkRef, + "bc-endpoint-contract/input.luac::root@pc1", + "bc-endpoint-contract/input.luac::root@pc2", + "luci.http.formvalue", + "os.execute", + counters + ) + val semantics = LuaProgramSemantics( + moduleResolutions = Vector.empty, + moduleReturnTables = Vector.empty, + moduleFieldCallTargets = Vector.empty, + interproceduralArgFlows = Vector.empty, + interproceduralReturnFlows = Vector.empty, + crossBoundaryCallTargets = Vector.empty, + taintPaths = + Vector(LuaTaintPath(sourceRef, sinkRef, Vector(sourceRef, sinkRef), "true-positive", "bytecode-only")), + boundaries = Vector.empty, + ruleMatches = Vector.empty, + sourceEndpoints = Vector.empty, + sinkEndpoints = Vector.empty, + sanitizerCalls = Vector.empty, + sanitizerClassifications = Vector.empty, + reportClassifications = Vector.empty, + vulnerabilityReports = Vector.empty, + e5Boundaries = Vector.empty, + pathSearchStats = LuaPathSearchStats(0, 0, 1, 1, 1, 1, 0), + performanceAttribution = LuaPerformanceAttribution(1, 0, 1, 0, Vector(pair), counters) + ) + + FileUtil.usingTemporaryDirectory("lua2cpg-endpoint-contract") { tmpDir => + val exportDir = tmpDir.resolve("must-not-exist") + val error = intercept[IllegalStateException] { + LuaRealFirmwareEvidenceExporter.write( + Config(realFirmwareOutputDir = Some(exportDir.toString)), + Vector.empty, + semantics + ) + } + error.getMessage should include("taint path lacks source endpoint") + Files.exists(exportDir) shouldBe false + } + } + + "export OpenWrt-derived source and sink endpoints" in { + withOpenWrtDerivedExportDir { exportDir => + val rows = stagingRows(exportDir) + val sourceRows = rows.flatMap(_("source_endpoints").arr.map(_.obj)) + val sinkRows = rows.flatMap(_("sink_endpoints").arr.map(_.obj)) + val pathRows = rows.flatMap(_("path_evidence").arr.map(_.obj)) + + sourceRows.exists(row => + row("module_path").str.endsWith("luci/controller/mtkwifi.luac") && + hasScopedCallsite(row, "root.110@pc3") && + row("trigger").str == "luci.http.formvalue" + ) shouldBe true + + sinkRows.exists(row => + row("module_path").str.endsWith("luci/controller/mtkwifi.luac") && + hasScopedCallsite(row, "root.110@pc12") && + row("trigger").str == "os.execute" + ) shouldBe true + + sinkRows.exists(row => + row("module_path").str.endsWith("luci/controller/hwnat.luac") && + row("trigger").str == "io.popen" + ) shouldBe true + + pathRows.exists(row => + row("source_module_path").str.endsWith("luci/controller/mtkwifi.luac") && + row("sink_module_path").str.endsWith("luci/controller/mtkwifi.luac") && + row("source_pc").num.toInt == 3 && + row("sink_pc").num.toInt == 12 && + row("path_steps").arr.exists(_.str.endsWith("luci/controller/mtkwifi.luac::root.110@pc3:r0")) && + row("path_steps").arr.exists(_.str.endsWith("luci/controller/mtkwifi.luac::root.110@pc12:r1")) + ) shouldBe true + } + } + + "export OpenWrt-derived path report totals with scoped path steps" in { + withOpenWrtDerivedExportDir { exportDir => + val profile = ujson.read(Files.readString(exportDir.resolve("path-search-profile.json"))).obj + profile("taint_path_count").num.toInt shouldBe 20 + profile("report_count").num.toInt shouldBe 20 + + val pathRows = stagingRows(exportDir).flatMap(_("path_evidence").arr.map(_.obj)) + pathRows.size shouldBe 20 + pathRows.foreach { row => + row("source_module_path").str should not be empty + row("sink_module_path").str should not be empty + row("path_steps").arr should not be empty + row("path_steps").arr.foreach { step => + step.str should include("::") + } + } + pathRows.exists(row => row.obj.contains("callsite_id")) shouldBe false + } + } + } + + private def withOpenWrtDerivedExportDir(test: Path => Unit): Unit = { + val resourceRoot = Paths.get(getClass.getClassLoader.getResource("openwrt-derived-firmware-lua/usr/lib/lua").toURI) + + FileUtil.usingTemporaryDirectory("lua2cpg-openwrt-derived-real-firmware-export") { tmpDir => + val outputPath = tmpDir.resolve("openwrt-derived-firmware-lua.cpg.bin").toString + val exportDir = tmpDir.resolve("openwrt-derived-firmware-lua-evidence") + val cpg = new Lua2Cpg() + .createCpg( + Config(realFirmwareOutputDir = Some(exportDir.toString)) + .withInputPath(resourceRoot.toString) + .withOutputPath(outputPath) + ) + .get + cpg.close() + + test(exportDir) + } + } + + private def stagingRows(exportDir: Path) = { + val stagingStream = Files.list(exportDir.resolve("staging")) + try { + val rows = stagingStream.iterator.asScala.toVector.map(path => ujson.read(Files.readString(path)).obj) + rows should not be empty + rows + } finally { + stagingStream.close() + } + } + + private def hasScopedCallsite(row: ujson.Obj, localCallsiteId: String): Boolean = + row("callsite_id").str.contains("::") && row("callsite_id").str.endsWith(s"::$localCallsiteId") +} diff --git a/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/RulesSanitizerReportSmokeTest.scala b/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/RulesSanitizerReportSmokeTest.scala new file mode 100644 index 000000000000..b218fa24d054 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/RulesSanitizerReportSmokeTest.scala @@ -0,0 +1,104 @@ +package io.joern.lua2cpg + +import io.shiftleft.codepropertygraph.cpgloading.CpgLoader +import io.shiftleft.semanticcpg.language.* +import io.shiftleft.semanticcpg.utils.FileUtil +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +import java.nio.file.Paths + +class RulesSanitizerReportSmokeTest extends AnyWordSpec with Matchers { + + "Lua2Cpg" should { + "emit rules sanitizer and report markers that survive CPG reopen" in { + val resourceRoot = Paths.get(getClass.getClassLoader.getResource("rules-sanitizer-report").toURI) + + FileUtil.usingTemporaryDirectory("lua2cpg-rules-sanitizer-report-smoke") { tmpDir => + val outputPath = tmpDir.resolve("rules-sanitizer-report.cpg.bin").toString + val cpg = new Lua2Cpg() + .createCpg(Config().withInputPath(resourceRoot.toString).withOutputPath(outputPath)) + .get + cpg.close() + + val reopened = CpgLoader.load(outputPath) + try { + markerCodes(reopened, "lua.rule.match") should contain allOf ( + "d16-rf-formvalue-os-execute-chain/input.luac:root@pc16 formvalue -> luci.http.formvalue", + "d16-rf-formvalue-os-execute-chain/input.luac:root@pc20 execute -> os.execute", + "d16-rf-webcmd-cross-module-popen/mtkwifi.luac:root.1@pc3 popen -> io.popen" + ) + markerCodes(reopened, "lua.source.endpoint") should contain allOf ( + "d16-rf-formvalue-os-execute-chain/input.luac:root@pc16:r0 via luci.http.formvalue", + "d16-rf-webcmd-cross-module-popen/controller.luac:root.1@pc4:r0 via luci.http.formvalue" + ) + markerCodes(reopened, "lua.sink.endpoint") should contain allOf ( + "d16-rf-formvalue-os-execute-chain/input.luac:root@pc20:r2 via os.execute param=0", + "d16-rf-webcmd-cross-module-popen/mtkwifi.luac:root.1@pc3:r2 via io.popen param=0" + ) + + markerCodes(reopened, "lua.sanitizer.call") should contain allOf ( + "d24-sanitizer-suppresses-report/input.luac:root@pc19 tonumber -> d24-sanitizer-suppresses-report/input.luac:root@pc19:r1", + "d24-sanitizer-same-suffix-off-chain-negative/input.luac:root@pc20 tonumber -> d24-sanitizer-same-suffix-off-chain-negative/input.luac:root@pc20:r2" + ) + val genericSanitizerCalls = markerCodes(reopened, "lua.sanitizer.call") + .filter(_.contains("sanitizer-rule-generic/nested/controller.luac")) + withClue(s"generic sanitizer calls: ${genericSanitizerCalls.mkString(", ")}") { + genericSanitizerCalls.exists(code => code.contains("_cmdformat") && code.contains("->")) shouldBe true + } + markerCodes(reopened, "lua.sanitizer.classification") should contain allOf ( + "d24-sanitizer-suppresses-report/input.luac:root@pc16:r0 -> d24-sanitizer-suppresses-report/input.luac:root@pc23:r3 classification=sanitized sanitizer=tonumber", + "d24-sanitizer-same-suffix-off-chain-negative/input.luac:root@pc16:r0 -> d24-sanitizer-same-suffix-off-chain-negative/input.luac:root@pc24:r3 classification=not-sanitized sanitizer=tonumber" + ) + markerCodes(reopened, "lua.report.classification") should contain allOf ( + "d24-sanitizer-suppresses-report/input.luac:root@pc16:r0 -> d24-sanitizer-suppresses-report/input.luac:root@pc23:r3 classification=sanitized reason=on-chain-sanitizer", + "d24-sanitizer-same-suffix-off-chain-negative/input.luac:root@pc16:r0 -> d24-sanitizer-same-suffix-off-chain-negative/input.luac:root@pc24:r3 classification=true-positive reason=no-on-chain-sanitizer" + ) + markerCodes(reopened, "lua.report.vulnerability") should contain allOf ( + "d16-rf-formvalue-os-execute-chain/input.luac:root@pc16:r0 -> d16-rf-formvalue-os-execute-chain/input.luac:root@pc20:r2 status=path-proven classification=true-positive path=d16-rf-formvalue-os-execute-chain/input.luac:root@pc16:r0;d16-rf-formvalue-os-execute-chain/input.luac:root@pc19:r0;d16-rf-formvalue-os-execute-chain/input.luac:root@pc19:r2;d16-rf-formvalue-os-execute-chain/input.luac:root@pc20:r2", + "d16-rf-submit-dpp-uri-execute/input.luac:root.2@pc4:r0 -> d16-rf-submit-dpp-uri-execute/input.luac:root.2@pc8:r2 status=path-proven classification=true-positive path=d16-rf-submit-dpp-uri-execute/input.luac:root.2@pc4:r0;d16-rf-submit-dpp-uri-execute/input.luac:root.2@pc7:r0;d16-rf-submit-dpp-uri-execute/input.luac:root.2@pc7:r2;d16-rf-submit-dpp-uri-execute/input.luac:root.2@pc8:r2", + "d16-rf-webcmd-cross-module-popen/controller.luac:root.1@pc4:r0 -> d16-rf-webcmd-cross-module-popen/mtkwifi.luac:root.1@pc3:r2 status=path-proven classification=true-positive path=d16-rf-webcmd-cross-module-popen/controller.luac:root.1@pc4:r0;d16-rf-webcmd-cross-module-popen/controller.luac:root.1@pc7:r0;d16-rf-webcmd-cross-module-popen/controller.luac:root.1@pc7:r2;d16-rf-webcmd-cross-module-popen/controller.luac:root.1@pc8:r2;d16-rf-webcmd-cross-module-popen/mtkwifi.luac:root.1:r0;d16-rf-webcmd-cross-module-popen/mtkwifi.luac:root.1@pc2:r0;d16-rf-webcmd-cross-module-popen/mtkwifi.luac:root.1@pc2:r2;d16-rf-webcmd-cross-module-popen/mtkwifi.luac:root.1@pc3:r2" + ) + val ruleCodes = markerCodes(reopened, "lua.rule.match") + ruleCodes.exists(_.contains("formvaluex")) shouldBe false + ruleCodes.exists(_.contains("executex")) shouldBe false + markerCodes(reopened, "lua.sink.endpoint") + .exists(_.contains("d24-rules-overmatch-constant-sink-negative")) shouldBe false + markerCodes(reopened, "lua.sanitizer.call") + .exists(_.contains("sanitizer-rule-lookalike")) shouldBe false + + markerCodes(reopened, "lua.report.vulnerability") + .exists(_.contains("d24-sanitizer-suppresses-report")) shouldBe false + markerCodes(reopened, "lua.report.classification") + .exists(code => + code.contains("sanitizer-rule-generic") && code.contains("classification=sanitized") + ) shouldBe true + markerCodes(reopened, "lua.report.vulnerability") + .exists(_.contains("sanitizer-rule-generic")) shouldBe false + markerCodes(reopened, "lua.report.vulnerability") + .exists(_.contains("sanitizer-rule-lookalike")) shouldBe true + markerCodes(reopened, "lua.report.vulnerability") + .exists(_.contains("d24-report-no-report-without-path-negative")) shouldBe false + markerCodes(reopened, "lua.report.vulnerability") + .exists(code => code.contains("bc-kill-overwrite") || code.contains("bc-branch-negative")) shouldBe false + + val e5NodeCount = reopened.call + .name( + "lua\\.(rule\\.match|source\\.endpoint|sink\\.endpoint|sanitizer\\.call|sanitizer\\.classification|report\\.classification|report\\.vulnerability|e5\\.boundary)" + ) + .size + val reportCount = reopened.call.nameExact("lua.report.vulnerability").size + info(s"e5_node_count=$e5NodeCount") + info(s"e5_report_count=$reportCount") + e5NodeCount should be > 0 + reportCount should be > 0 + } finally { + reopened.close() + } + } + } + } + + private def markerCodes(cpg: io.shiftleft.codepropertygraph.generated.Cpg, name: String): List[String] = + cpg.call.nameExact(name).code.l +} diff --git a/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/bytecode/LuaBytecodeDecoderTest.scala b/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/bytecode/LuaBytecodeDecoderTest.scala new file mode 100644 index 000000000000..305133d72b47 --- /dev/null +++ b/joern-cli/frontends/lua2cpg/src/test/scala/io/joern/lua2cpg/bytecode/LuaBytecodeDecoderTest.scala @@ -0,0 +1,84 @@ +package io.joern.lua2cpg.bytecode + +import io.joern.lua2cpg.bytecode.LuaConstantValue.NumberValue +import io.joern.lua2cpg.bytecode.LuaConstantValue.StringValue +import org.scalatest.matchers.should.Matchers +import org.scalatest.wordspec.AnyWordSpec + +class LuaBytecodeDecoderTest extends AnyWordSpec with Matchers { + + "LuaBytecodeDecoder" should { + "decode nested prototype identity and parameter counts" in { + val result = decodeResource("bytecode-model/bc-prototype-params/input.luac") + val root = acceptedRoot(result) + + root.nested.size.shouldBe(1) + val nestedPrototype = root.nested.head + nestedPrototype.prototypeId.shouldBe("root.0") + nestedPrototype.parentPrototypeId.shouldBe(Some("root")) + nestedPrototype.numParams.shouldBe(2) + } + + "decode string and number constants used by call fixtures" in { + val result = decodeResource("bytecode-model/bc-constants-call/input.luac") + val root = acceptedRoot(result) + + val constants = root.constants.map(constant => constant.luaType -> constant.value) + constants.contains("string" -> StringValue("alpha")).shouldBe(true) + constants.contains("number" -> NumberValue(7.0)).shouldBe(true) + } + + "accept stripped metadata bytecode while preserving structural bytecode facts" in { + val result = decodeResource("bytecode-model/bc-stripped-metadata/input.luac") + val root = acceptedRoot(result) + + root.prototypeId.shouldBe("root") + root.nested.map(_.prototypeId).contains("root.0").shouldBe(true) + root.constants + .map(constant => constant.luaType -> constant.value) + .contains("string" -> StringValue("metadata")) + .shouldBe(true) + root.locals.isEmpty.shouldBe(true) + root.upvalueNames.isEmpty.shouldBe(true) + } + + "return diagnostics without accepted prototype models for malformed inputs" in { + val cases = Seq( + "not-lua-bytecode.luac" -> "not-lua-bytecode", + "truncated.luac" -> "truncated-bytecode", + "unsupported-version.luac" -> "unsupported-bytecode-version", + "unsupported-profile.luac" -> "unsupported-bytecode-profile", + "malformed-constant.luac" -> "malformed-constant" + ) + + cases.foreach { case (fileName, expectedKind) => + val result = decodeResource(s"bytecode-model/bc-malformed-diagnostic/$fileName") + + result.artifact.accepted.shouldBe(false) + result.artifact.diagnostic.kind.shouldBe(expectedKind) + result.artifact.diagnostic.severity.shouldBe("error") + result.artifact.diagnostic.successFactsAllowed.shouldBe(false) + result.root.shouldBe(None) + } + } + } + + private def acceptedRoot(result: LuaBytecodeDecodeResult): LuaPrototype = { + result.artifact.accepted.shouldBe(true) + result.artifact.diagnostic.kind.shouldBe("accepted") + result.artifact.diagnostic.successFactsAllowed.shouldBe(true) + result.profile.isDefined.shouldBe(true) + result.root.get + } + + private def decodeResource(path: String): LuaBytecodeDecodeResult = + LuaBytecodeDecoder.decode(path, readResourceBytes(path)) + + private def readResourceBytes(path: String): Array[Byte] = { + val stream = Option(getClass.getClassLoader.getResourceAsStream(path)).getOrElse { + fail(s"Missing lua2cpg bytecode test resource: $path") + } + try stream.readAllBytes() + finally stream.close() + } +} diff --git a/project/Projects.scala b/project/Projects.scala index 8d98ce0ac763..dfdf4e18bb5e 100644 --- a/project/Projects.scala +++ b/project/Projects.scala @@ -26,6 +26,7 @@ object Projects { lazy val csharpsrc2cpg = project.in(frontendsRoot / "csharpsrc2cpg") lazy val abap2cpg = project.in(frontendsRoot / "abap2cpg") lazy val rust2cpg = project.in(frontendsRoot / "rust2cpg") + lazy val lua2cpg = project.in(frontendsRoot / "lua2cpg") lazy val linterRules = project.in(file("linter-rules"))