package javascript
import (
"bytes"
"encoding/json"
"fmt"
"io"
"sort"
"strings"
"github.com/Masterminds/semver/v3"
"github.com/xmirrorsecurity/opensca-cli/v3/opensca/common"
"github.com/xmirrorsecurity/opensca-cli/v3/opensca/logs"
"github.com/xmirrorsecurity/opensca-cli/v3/opensca/model"
"github.com/xmirrorsecurity/opensca-cli/v3/opensca/sca/cache"
)
type PackageJson struct {
Name string `json:"name"`
Version string `json:"version"`
Develop bool `json:"dev"`
Resolutions map[string]string `json:"resolutions"`
Dependencies map[string]string `json:"dependencies"`
DevDependencies map[string]string `json:"devDependencies"`
PeerDependencies map[string]string `json:"peerDependencies"`
OptionalDependencies map[string]string `json:"optionalDependencies"`
PeerDependenciesMeta map[string]struct {
Optional bool `json:"optional"`
} `json:"peerDependenciesMeta"`
File *model.File `json:"-"`
}
type NpmJson struct {
Time map[string]string `json:"time"`
Versions map[string]*PackageJson `json:"versions"`
}
type PackageLock struct {
Name string `json:"name"`
LockfileVersion int `json:"lockfileVersion"`
Version string `json:"version"`
Dependencies map[string]*PackageLockDep `json:"dependencies"`
Packages map[string]*PackageJson `json:"packages"`
}
type PackageLockDep struct {
name string
Version string `json:"version"`
Develop bool `json:"dev"`
Requires map[string]string `json:"requires"`
Dependencies map[string]*PackageLockDep `json:"dependencies"`
}
func npmkey(name, version string) string {
return fmt.Sprintf("%s:%s", name, version)
}
func _depSet() *model.DepGraphMap {
return model.NewDepGraphMap(nil, func(s ...string) *model.DepGraph {
return &model.DepGraph{
Name: s[0],
Version: s[1],
Develop: len(s) > 2 && s[2] == "dev",
}
})
}
func readJson[T any](reader io.Reader) *T {
var data T
err := json.NewDecoder(reader).Decode(&data)
if err != nil {
return nil
}
return &data
}
var npmOrigin = func(name, version string) *PackageJson {
var origin *PackageJson
path := cache.Path("", name, version, model.Lan_JavaScript)
cache.Load(path, func(reader io.Reader) {
origin = ReadNpmJson(reader, version)
})
if origin != nil {
return origin
}
common.DownloadUrlFromRepos(name, func(repo common.RepoConfig, r io.Reader) {
data, err := io.ReadAll(r)
if err != nil {
logs.Warn(err)
return
}
reader := bytes.NewReader(data)
origin = ReadNpmJson(reader, version)
reader.Seek(0, io.SeekStart)
cache.Save(path, reader)
}, defaultNpmRepo...)
return origin
}
func RegisterNpmOrigin(origin func(name, version string) *PackageJson) {
if origin != nil {
npmOrigin = origin
}
}
func ParsePackageJsonWithNode(pkgjson *PackageJson, nodeMap map[string]*PackageJson, pkgMap map[string]*PackageJson) *model.DepGraph {
_dep := _depSet().LoadOrStore
findDep := func(dev bool, name, version, basedir string) *model.DepGraph {
var subjs *PackageJson
if subjs == nil && len(nodeMap) > 0 {
_, subjs = findFromNodeModules(name, basedir, nodeMap)
}
if subjs == nil && len(pkgMap) > 0 {
if c, err := semver.NewConstraint(version); err == nil {
if pkg, ok := pkgMap[name]; ok {
if v, err := semver.NewVersion(pkg.Version); err == nil {
if c.Check(v) {
subjs = pkg
}
}
}
}
}
if subjs == nil {
subjs = npmOrigin(name, version)
}
if subjs == nil {
subjs = &PackageJson{
Name: name,
Version: version,
}
}
var dep *model.DepGraph
if dev {
dep = _dep(subjs.Name, subjs.Version, "dev")
} else {
dep = _dep(subjs.Name, subjs.Version)
}
if dep.Expand == nil {
dep.Expand = subjs
}
return dep
}
root := &model.DepGraph{Name: pkgjson.Name, Version: pkgjson.Version, Path: pkgjson.File.Relpath()}
root.Expand = pkgjson
for name, version := range pkgjson.DevDependencies {
root.AppendChild(findDep(true, name, version, pkgjson.File.Relpath()))
}
root.ForEachPath(func(p, n *model.DepGraph) bool {
js := n.Expand.(*PackageJson)
for name, version := range js.Dependencies {
n.AppendChild(findDep(false, name, version, js.File.Relpath()))
}
return true
})
root.ForEachNode(func(p, n *model.DepGraph) bool { n.Expand = nil; return true })
return root
}
func ParsePackageJsonWithLock(pkgjson *PackageJson, pkglock *PackageLock) *model.DepGraph {
if pkglock.LockfileVersion == 3 {
return ParsePackageJsonWithLockV3(pkgjson, pkglock)
}
depNameMap := map[string]*model.DepGraph{}
devDepNameMap := map[string]*model.DepGraph{}
_dep := _depSet().LoadOrStore
for name, lockDep := range pkglock.Dependencies {
if lockDep.Develop {
devDepNameMap[name] = _dep(name, lockDep.Version, "dev")
} else {
depNameMap[name] = _dep(name, lockDep.Version)
}
}
for name, lockDep := range pkglock.Dependencies {
lockDep.name = name
q := []*PackageLockDep{lockDep}
for len(q) > 0 {
n := q[0]
q = q[1:]
var dep *model.DepGraph
if n.Develop {
dep = _dep(n.name, n.Version, "dev")
} else {
dep = _dep(n.name, n.Version)
}
dup := map[string]bool{}
for name, sub := range n.Dependencies {
dup[name] = true
sub.name = name
q = append(q, sub)
if sub.Develop || n.Develop {
dep.AppendChild(_dep(name, sub.Version, "dev"))
} else {
dep.AppendChild(_dep(name, sub.Version))
}
}
for name := range n.Requires {
if dup[name] {
continue
}
if n.Develop {
dep.AppendChild(devDepNameMap[name])
} else {
dep.AppendChild(depNameMap[name])
}
}
}
}
root := &model.DepGraph{Name: pkgjson.Name, Version: pkgjson.Version, Path: pkgjson.File.Relpath()}
for name := range pkgjson.Dependencies {
root.AppendChild(depNameMap[name])
}
for name := range pkgjson.DevDependencies {
root.AppendChild(devDepNameMap[name])
}
return root
}
func ParsePackageJsonWithLockV3(pkgjson *PackageJson, pkglock *PackageLock) *model.DepGraph {
if pkglock.LockfileVersion != 3 {
return nil
}
for jspath, js := range pkglock.Packages {
if js.File == nil {
js.File = model.NewFile("", jspath)
}
}
root := &model.DepGraph{Name: pkgjson.Name, Version: pkgjson.Version, Path: pkgjson.File.Relpath()}
type expand struct {
js *PackageJson
path string
}
root.Expand = expand{js: pkgjson, path: ""}
_dep := model.NewDepGraphMap(nil, func(s ...string) *model.DepGraph { return &model.DepGraph{Name: s[0], Version: s[1]} }).LoadOrStore
findDep := func(name, basedir string) *model.DepGraph {
jspath, subjs := findFromNodeModules(name, basedir, pkglock.Packages)
if subjs == nil {
return nil
}
dep := _dep(name, subjs.Version, subjs.File.Relpath())
if dep.Expand == nil {
dep.Develop = subjs.Develop
dep.Expand = expand{
path: jspath,
js: subjs,
}
} else {
if !subjs.Develop {
dep.Develop = false
}
}
return dep
}
root.ForEachPath(func(p, n *model.DepGraph) bool {
njs := n.Expand.(expand)
if njs.js == nil {
return false
}
for name := range njs.js.Dependencies {
n.AppendChild(findDep(name, njs.path))
}
for name := range njs.js.PeerDependencies {
if meta, ok := njs.js.PeerDependenciesMeta[name]; ok {
if meta.Optional {
continue
}
}
n.AppendChild(findDep(name, njs.path))
}
for name := range njs.js.OptionalDependencies {
n.AppendChild(findDep(name, njs.path))
}
for name := range njs.js.DevDependencies {
dep := findDep(name, njs.path)
if dep != nil {
dep.Develop = true
n.AppendChild(dep)
}
}
return true
})
return root
}
func ReadNpmJson(reader io.Reader, version string) *PackageJson {
npm := readJson[NpmJson](reader)
if npm == nil {
return nil
}
vers := []string{}
for v := range npm.Versions {
vers = append(vers, v)
}
return npm.Versions[FindMaxVersion(version, vers)]
}
func FindMaxVersion(version string, versions []string) string {
c, err := semver.NewConstraint(version)
if err != nil {
return version
}
vers := []*semver.Version{}
for _, v := range versions {
ver, err := semver.NewVersion(v)
if err != nil {
continue
}
if c.Check(ver) {
vers = append(vers, ver)
}
}
sort.Slice(vers, func(i, j int) bool {
return vers[i].Compare(vers[j]) > 0
})
if len(vers) > 0 {
return vers[0].String()
}
return version
}
func findFromNodeModules(name, basedir string, nodePathMap map[string]*PackageJson) (jspath string, js *PackageJson) {
const node_modules = "node_modules"
paths := strings.Split(strings.ReplaceAll(basedir, `\`, `/`), "/")
for i := range paths {
tail := len(paths) - i
dirs := paths[:tail]
if paths[tail-1] != node_modules {
dirs = append(dirs, node_modules)
}
dirs = append(dirs, name)
jspath = strings.TrimLeft(strings.Join(dirs, "/"), "/")
if js, ok := nodePathMap[jspath]; ok {
return jspath, js
}
}
return
}