#!/usr/bin/env python
"""
When I perform a secure code review assessment, often, the code provided use internal dependencies that are not provided.
Therefore, I cannot grab all dependencies artifacts locally in order to check for presence of vulnerable dependencies 
via OWASP Dependency Check.

This script analyse all the "pom.xml" descriptor file present recursively in a folder and create a single "pom.xml" with all dependencies 
resolvable via the online official registry.

It leverage the data provided by the Google site "https://deps.dev/".

Other type of project will be added with the time based on the case meet :)

Dependency: pip install requests termcolor
"""
import requests
import argparse
import pathlib
import xml.etree.ElementTree as ET
from termcolor import colored

MAVEN_PROJECT_DESCRIPTOR_TPL = """<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<groupId>myGroup</groupId>
	<artifactId>MyProject</artifactId>
	<version>1.0.0-SNAPSHOT</version>
    <dependencies>%s</dependencies>
    <!-- in case of artifacts not found in Maven main repository -->
    <repositories>
        <repository>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
            <id>jcenter-releases</id>
            <name>jcenter</name>
            <url>http://jcenter.bintray.com</url>
        </repository>
        <repository>
            <snapshots>
                <enabled>false</enabled>
            </snapshots>
            <id>maven-releases</id>
            <name>maven</name>
            <url>https://repo.maven.apache.org/maven2</url>
        </repository>	
    </repositories>
    <!-- in case of multi-modules projects -->
    <properties>%s</properties>
</project>    
"""

MAVEN_DEPENDENCY_TPL = "<dependency><groupId>%s</groupId><artifactId>%s</artifactId><version>%s</version></dependency>"


def list_packages_maven(xml_content, gids_to_ignore):
    namespaces = {"mvn": "http://maven.apache.org/POM/4.0.0"}
    dependencies = []
    root = ET.fromstring(xml_content)
    dep_nodes = root.findall(".//mvn:dependency", namespaces)
    for dep_node in dep_nodes:
        try:
            group_id = dep_node.find("mvn:groupId", namespaces).text
            if group_id in gids_to_ignore or group_id.startswith("${project."):
                continue
            artifact_id = dep_node.find("mvn:artifactId", namespaces).text
            version = dep_node.find("mvn:version", namespaces).text
            pkg_name = f"{group_id}:{artifact_id}:{version}"
            if pkg_name not in dependencies:
                dependencies.append(pkg_name)
        except:
            continue
    return dependencies


def list_properties_maven(xml_content):
    namespaces = {"mvn": "http://maven.apache.org/POM/4.0.0"}
    properties = {}
    root = ET.fromstring(xml_content)
    props_nodes = root.findall(".//mvn:properties", namespaces)
    for props_node in props_nodes:
        try:
            for prop in props_node:
                n = prop.tag.replace("{%s}" % namespaces['mvn'], "")
                v = prop.text
                properties[n] = v
        except:
            continue
    return properties


def find_package_present_into_registry(pkg_names_list, registry_name):
    present = []
    with requests.Session() as session:
        for pkg_name in pkg_names_list:
            parts = pkg_name.split(":")
            pkg_name_only = f"{parts[0]}:{parts[1]}"
            version_only = parts[2]
            if version_only.startswith("${"):
                present.append(pkg_name)
                continue
            print(f"\rPackage {pkg_name:<75}", end="", flush=True)
            name_encoded = pkg_name_only.replace(
                "/", "%2F").replace("@", "%40").replace(":", "%3A")
            u = f"https://deps.dev/_/s/{registry_name}/p/{name_encoded}/v/{version_only}"
            response = session.get(url=u)
            if response.status_code == 200 and "version" in response.json():
                present.append(pkg_name)
    present.sort()
    return present


def generate_project_descriptor(project_descriptor_file_name, pkg_names_list, global_properties, project_type):
    if project_type == "maven":
        dependencies = []
        for pkg_name in pkg_names_list:
            parts = pkg_name.split(":")
            dependencies.append(MAVEN_DEPENDENCY_TPL % (parts[0], parts[1], parts[2]))
        properties = ""
        for prop_name, prop_value in global_properties.items():
            properties += f"<{prop_name}>{prop_value}</{prop_name}>\n"
        with open(project_descriptor_file_name, mode="w", encoding="utf-8") as f:
            f.write(MAVEN_PROJECT_DESCRIPTOR_TPL % ("\n".join(dependencies), properties))


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Generate a valid project descriptor with all dependencies resolvable via online official registry.")
    required_params = parser.add_argument_group("required named arguments")
    required_params.add_argument("-f", action="store", dest="base_folder", help="Path to folder containing the project code base.", required=True)
    required_params.add_argument("-t", action="store", dest="project_type", choices=["maven"], help="System managing the project.", required=True)
    parser.add_argument("-e", action="store", dest="gids_to_ignore", help="List of artefacts GroupID, separated by a comma, to ignores (excludes) from the final POM.", required=False, default="")
    args = parser.parse_args()
    project_descriptor_file_name = None
    gids_to_ignore = args.gids_to_ignore.split(",")
    if args.project_type == "maven":
        project_descriptor_file_name = "pom.xml"
    print(colored(f"[+] Extract all dependencies from all '{project_descriptor_file_name}' files...", "yellow"))
    global_dependencies = []
    global_properties = {}
    project_descriptor_file_name_file_count = 0
    for prj_file in pathlib.Path(args.base_folder).rglob(project_descriptor_file_name):
        with open(prj_file, mode="r", encoding="utf-8") as f:
            content = f.read()
            project_descriptor_file_name_file_count += 1
        if args.project_type == "maven":
            dependencies = list_packages_maven(content, gids_to_ignore)
            properties = list_properties_maven(content)
            global_properties.update(properties)
            global_dependencies.extend(dependencies)
    global_dependencies = list(set(global_dependencies))
    print(f"\rDependencies identified ({project_descriptor_file_name_file_count} files read): {len(global_dependencies):<80}")
    print(colored(f"[+] Identify all resolvable dependencies...", "yellow"))
    resolvable_dependencies = find_package_present_into_registry(global_dependencies, args.project_type)
    print(f"\rDependencies identified: {len(resolvable_dependencies):<80}")
    print(colored(f"[+] Generate project descriptor named file '{project_descriptor_file_name}' in current folder...", "yellow"))
    generate_project_descriptor(project_descriptor_file_name, resolvable_dependencies, global_properties, args.project_type)
