Compare commits

...

5 Commits
main ... dev

Author SHA1 Message Date
Christian Risi
28fa065ef9 V 0.1.0: Tests (Adelosina intricata) 2025-05-15 12:02:13 +02:00
Christian Risi
b2cc65ef2c Merge branch 'dev' of https://repositories.communitynotfound.work/Py-Docktor/DocktorAnalyzer into dev 2025-05-15 11:56:38 +02:00
Christian Risi
06b344dfac V 0.1.0: Source (Adelosina intricata) 2025-05-15 11:52:57 +02:00
Christian Risi
13d8399d4a V 0.1.0: Source (Adelosina intricata) 2025-05-15 11:50:11 +02:00
Christian Risi
adedc2277a Added Assets 2025-05-15 11:38:56 +02:00
20 changed files with 645 additions and 0 deletions

5
.pypirc Normal file
View File

@ -0,0 +1,5 @@
[distutils]
index_servers =
private-gitea
[private-gitea]

0
assets/.gitkeep Normal file
View File

View File

View File

View File

@ -0,0 +1,7 @@
FROM fedora:41
RUN dnf install -y socat
WORKDIR /service/
ADD ./okay /service/okay
RUN chmod +x ./okay
ENTRYPOINT ["socat", "-d", "TCP-LISTEN:3000,reuseaddr,fork", "EXEC:timeout -k 5 30 ./okay"]

View File

@ -0,0 +1,9 @@
FROM python:3.6-slim
RUN apt-get update && apt-get install -y socat gcc g++ make libffi-dev libssl-dev
WORKDIR /service/
COPY ./requirements.txt ./requirements.txt
RUN pip install -r requirements.txt
ADD ./main.py ./main.py
RUN chmod +x main.py
ENTRYPOINT socat -d TCP-LISTEN:3000,reuseaddr,fork EXEC:'timeout -k 5 30 python3 -u main.py'

View File

@ -0,0 +1,20 @@
FROM docker.io/oven/bun AS frontend
WORKDIR /app
COPY frontend/package*.json ./
RUN bun i
COPY frontend/ .
RUN bun run build
FROM python:3.12-slim
ENV DOCKER=1
WORKDIR /app
COPY ./backend/requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY ./backend/ .
COPY --from=frontend /app/dist /app/frontend
CMD ["python3", "app.py"]

View File

@ -0,0 +1,25 @@
FROM docker.io/oven/bun AS frontend
WORKDIR /app
COPY frontend/package*.json ./
RUN bun i
COPY frontend/ .
RUN bun run build
FROM python:3.12-slim
ENV DOCKER=1
WORKDIR /app
COPY ./backend/requirements.txt \
.
RUN pip install --no-cache-dir -r \
requirements.txt
COPY ./backend/ \
\
\
.
COPY --from=frontend /app/dist /app/frontend
CMD ["python3", "app.py"]

View File

View File

@ -0,0 +1,6 @@
class NotImplemented(Exception):
def __init__(self, *args):
super().__init__(*args)
# TODO: Implement a way to print the method not impelemented

View File

@ -0,0 +1,29 @@
import re
import shlex
from docktoranalyzer.dockerfile.docker_constants import DockerConstants
def command_parser(command_line: str):
PARENTHESIS_OFFSET = 1
args: list[str] = []
array_insides: str = ""
command_line = command_line.strip()
arr_regex = re.compile(DockerConstants.EXEC_FORM_REGEX)
match = arr_regex.search(command_line)
if match is not None:
array_insides = command_line[match.start(): -1]
command_line = command_line[0:match.start() - PARENTHESIS_OFFSET]
args.extend(shlex.split(
command_line
))
args.extend(shlex.split(
array_insides
))
return args

View File

@ -0,0 +1,8 @@
from typing import Final
class DockerConstants:
LINE_CONTINUATION_REGEX: Final[str] = r"(^[\s]*\\[\s]*$|[\s]+\\[\s]*$)"
COMMENT_REGEX: Final[str] = r"[\s]*#"
EXEC_FORM_REGEX: Final[str] = r"(?<!\\\[)(?<=\[).*[^\\](?=\])"

View File

@ -0,0 +1,38 @@
from pathlib import Path
from docktoranalyzer.dockerfile.dockerfile_instructions import (
DockerInstruction,
)
# TODO: Make DockerStage have a DockerFS
class DockerStage:
"""_summary_
Class that holds a Build Stage
"""
def __init__(self, instructions: list[DockerInstruction]):
self.commands: list[DockerInstruction] = []
self.commands = instructions
class DockerFS:
"""_summary_
Class to take map file instructions
"""
def __init__(self, last_build_stage: DockerStage):
pass
class Dockerfile:
"""_summary_
Class holding a representation of Dockerfile
"""
def __init__(self, build_stages: list[DockerStage]):
self.stages: list[DockerStage] = []
self.fs: DockerFS
self.stages = build_stages
self.fs = DockerFS(self.stages[-1])

View File

@ -0,0 +1,238 @@
import re
import shlex
from docktoranalyzer.dockerfile.command_parser_utils import command_parser
from docktoranalyzer.dockerfile.docker_constants import DockerConstants
from docktoranalyzer.dockerfile.instruction_enums import DockerInstructionType
# UGLY: refactor this
def split_options_value(option: str) -> list[str]:
return option.split("=")
def split_args(command):
pass
# MARK: InstructionChunk
class InstructionChunk:
def __init__(self, chunk_lines: list[str]):
# UGLY: could preallocate space
self.lines: list[str] = []
tmp = chunk_lines.copy()
regex = re.compile(DockerConstants.LINE_CONTINUATION_REGEX)
for line in tmp:
line = regex.sub("", line)
self.lines.append(line)
def is_empty(self):
if len(self.lines[0]) == 0:
return True
return False
def __len__(self):
return len(self.lines)
def __str__(self):
pass
# MARK: DockerInstruction
class DockerInstruction:
"""_summary_
Base Structure for all docker instructions
"""
def __init__(self, chunk: list[str]):
self.type: DockerInstructionType = DockerInstructionType.UNPARSED
self.chunk: InstructionChunk = chunk
self.command: str = ""
self.args: str = ""
if self.chunk.is_empty():
self.type = DockerInstructionType.EMPTY
return
total_command = " ".join(self.chunk.lines)
command_words = command_parser(total_command)
self.command = command_words[0]
self.args = command_words[1:]
# MARK: COMMENT
class DockerCOMMENT(DockerInstruction):
def __init__(self, chunk: list[str]):
super().__init__(chunk)
self.type = DockerInstructionType.COMMENT
# TODO: Work on ADD
# MARK: ADD
class DockerADD(DockerInstruction):
# --key=value
__OPTIONS: set[str] = {
"--keep-git-dir",
"--checksum",
"--chown",
"--chmod",
"--link",
"--exclude",
}
def __init__(self, chunk: list[str]):
super().__init__(chunk)
self.options: list[str] = []
self.type = DockerInstructionType.ADD
self.remote: list[str] = []
self.local: list[str] = []
self.destination: str = ""
for arg in self.args:
if arg in DockerADD.__OPTIONS:
self.options.append(
arg
)
if "]" in arg or "[" in arg:
pass
class DockerARG(DockerInstruction):
def __init__(self, chunk: list[str]):
super().__init__(chunk)
# TODO: Work on CMD
# MARK: CMD
class DockerCMD(DockerInstruction):
def __init__(self, chunk: list[str]):
super().__init__(chunk)
self.type = DockerInstructionType.CMD
# TODO: Work on Copy
# MARK: COPY
class DockerCOPY(DockerInstruction):
# --key=value
__OPTIONS: set[str] = {
"--from",
"--chown",
"--chmod",
"--link",
"--parents",
"--exclude",
}
def __init__(self, chunk: list[str]):
super().__init__(chunk)
self.type = DockerInstructionType.COPY
# TODO: Work on ENTRYPOINT
class DockerENTRYPOINT(DockerInstruction):
def __init__(self, chunk: list[str]):
super().__init__(chunk)
self.type = DockerInstructionType.ENTRYPOINT
class DockerENV(DockerInstruction):
def __init__(self, chunk: list[str]):
super().__init__(chunk)
class DockerEXPOSE(DockerInstruction):
def __init__(self, chunk: list[str]):
super().__init__(chunk)
# TODO: Work on FROM
# MARK: FROM
class DockerFROM(DockerInstruction):
def __init__(self, chunk: list[str]):
super().__init__(chunk)
self.type = DockerInstructionType.FROM
class DockerHEALTHCHECK(DockerInstruction):
def __init__(self, chunk: list[str]):
super().__init__(chunk)
class DockerLABEL(DockerInstruction):
def __init__(self, chunk: list[str]):
super().__init__(chunk)
class DockerMAINTAINER(DockerInstruction):
def __init__(self, chunk: list[str]):
super().__init__(chunk)
class DockerONBUILD(DockerInstruction):
def __init__(self, chunk: list[str]):
super().__init__(chunk)
# TODO: Work on run
# MARK: RUN
class DockerRUN(DockerInstruction):
# This variable is only --key=value
__OPTIONS: set[str] = {"--mount", "--netowrk", "--security"}
def __init__(self, chunk: list[str]):
super().__init__(chunk)
self.type = DockerInstructionType.RUN
class DockerSHELL(DockerInstruction):
def __init__(self, chunk: list[str]):
super().__init__(chunk)
class DockerSTOPSIGNAL(DockerInstruction):
def __init__(self, chunk: list[str]):
super().__init__(chunk)
class DockerUSER(DockerInstruction):
def __init__(self, chunk: list[str]):
super().__init__(chunk)
class DockerVOLUME(DockerInstruction):
def __init__(self, chunk: list[str]):
super().__init__(chunk)
# TODO: Work on workdir
# MARK: WORKDIR
class DockerWORKDIR(DockerInstruction):
def __init__(self, chunk: list[str]):
super().__init__(chunk)
self.type = DockerInstructionType.WORKDIR
# TODO: Add workdirectory property

View File

@ -0,0 +1,194 @@
from pathlib import Path
import re
import shlex
from docktoranalyzer.dockerfile.docker_constants import DockerConstants
from docktoranalyzer.dockerfile.instruction_enums import DockerInstructionType
from docktoranalyzer.dockerfile.dockerfile_ import DockerStage, Dockerfile
from docktoranalyzer.dockerfile.dockerfile_instructions import (
DockerADD,
DockerCMD,
DockerCOMMENT,
DockerCOPY,
DockerENTRYPOINT,
DockerFROM,
DockerInstruction,
DockerRUN,
DockerWORKDIR,
InstructionChunk,
)
class DockerFileParser:
DEFAULT_ESCAPE_STRING = "\\"
DEFAULT_LINE_CONTINUATION = "\\"
DIRECTIVE_REGEX = re.compile(
"# +(?P<directive_name>[^\\s]*)=(?P<value>[^\\s]*)"
)
def __new__(cls):
raise TypeError("Static classes cannot be instantiated")
# MARK: dockerfile_factory()
@staticmethod
def dockerfile_factory(dockerfile_path: Path):
if not dockerfile_path.is_file():
raise FileNotFoundError(f"{dockerfile_path} is not a valid path")
dockerfile = dockerfile_path.open()
docker_instructions = dockerfile.readlines()
dockerfile.close()
chunks = DockerFileParser.__parse_chunks(docker_instructions)
instructions = DockerFileParser.__parse_instructions(chunks)
stages = DockerFileParser.__parse_stages(instructions)
return Dockerfile(stages.copy())
# MARK: __parse_directives()
@staticmethod
def __parse_directives(docker_lines: list[str]):
found_directives: set[str] = set()
for line in docker_lines:
line_length = len(line)
# Line is too short to
# make a directive
if line_length < 4:
continue
match = DockerFileParser.DIRECTIVE_REGEX.match(line)
# No match found
if match is None:
continue
directive_name = match.group("directive_name")
value = match.group("value")
# Duplicate directive, ignore
if directive_name in found_directives:
continue
found_directives.add(directive_name)
if directive_name == "escape":
DockerFileParser.ESCAPE_STRING = value
# MARK: __parse_chunks()
@staticmethod
def __parse_chunks(
instruction_lines: list[str],
line_continuation_regex: str = DockerConstants.LINE_CONTINUATION_REGEX,
) -> list[InstructionChunk]:
continuation_check = re.compile(line_continuation_regex)
comment_check = re.compile(DockerConstants.COMMENT_REGEX)
chunks: list[InstructionChunk] = []
accumulator: list[str] = []
for line in instruction_lines:
line = line.rstrip()
accumulator.append(line)
# If line is a comment, it can't continue
if comment_check.search(line) is not None:
if len(accumulator) > 1:
accumulator.pop()
chunks.append(InstructionChunk(accumulator))
chunks.append(InstructionChunk([line]))
accumulator = []
# If line doesn't continue, join everything found
if continuation_check.search(line) is None:
chunks.append(InstructionChunk(accumulator))
accumulator = []
return chunks
# MARK: __parse_instruction()
@staticmethod
def __parse_instructions(
instruction_chunks: list[InstructionChunk],
) -> list[DockerInstruction]:
docker_instructions: list[DockerInstruction] = []
for chunk in instruction_chunks:
docker_instructions.append(
DockerFileParser.__instruction_mapper(chunk)
)
return docker_instructions
# MARK: __instruction_mapper()
@staticmethod
def __instruction_mapper(
chunk: InstructionChunk,
) -> DockerInstruction:
if chunk.is_empty():
return DockerInstruction(chunk)
command = shlex.split(chunk.lines[0])[0]
if command == "#":
return DockerCOMMENT(chunk)
instruction_type: DockerInstructionType = DockerInstructionType[
f"{command}"
]
match instruction_type:
case DockerInstructionType.CMD:
return DockerCMD(chunk)
case DockerInstructionType.COPY:
return DockerCOPY(chunk)
case DockerInstructionType.ENTRYPOINT:
return DockerENTRYPOINT(chunk)
case DockerInstructionType.FROM:
return DockerFROM(chunk)
case DockerInstructionType.RUN:
return DockerRUN(chunk)
case DockerInstructionType.ADD:
return DockerADD(chunk)
case DockerInstructionType.WORKDIR:
return DockerWORKDIR(chunk)
case _:
return DockerInstruction(chunk)
# MARK: __parse_stages()
@staticmethod
def __parse_stages(
instructions: list[DockerInstruction],
) -> list[DockerStage]:
stages: list[DockerStage] = []
accumulator: list[DockerInstruction] = []
for instruction in instructions:
if instruction.type is DockerInstructionType.FROM:
stages.append(DockerStage(accumulator.copy()))
accumulator = []
accumulator.append(instruction)
stages.append(DockerStage(accumulator.copy()))
return stages

View File

@ -0,0 +1,30 @@
from enum import Enum, auto
class DockerInstructionType(Enum):
# Special values
UNPARSED = auto()
UNKOWN = auto()
EMPTY = auto()
COMMENT = auto()
# Docker Instructions
ADD = auto()
ARG = auto()
CMD = auto()
COPY = auto()
ENTRYPOINT = auto()
ENV = auto()
EXPOSE = auto()
FROM = auto()
HEALTHCHECK = auto()
LABEL = auto()
MAINTAINER = auto()
ONBUILD = auto()
RUN = auto()
SHELL = auto()
STOPSIGNAL = auto()
USER = auto()
VOLUME = auto()
WORKDIR = auto()

0
tests/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,36 @@
# content of test_sample.py
from pathlib import Path
import re
import pytest
import docktoranalyzer
import docktoranalyzer.dockerfile
from docktoranalyzer.dockerfile.docker_constants import DockerConstants
from docktoranalyzer.dockerfile.dockerfile_parser import DockerFileParser
# TODO: use a glob to take files and rege
@pytest.fixture
def docker_file_arrays():
# I need to count one stage over to account for instructions
# the first FROM
return [
{"path": "./assets/dockerfiles/binary.dockerfile", "stages": 2},
{"path": "./assets/dockerfiles/crypto.dockerfile", "stages": 2},
{"path": "./assets/dockerfiles/web.dockerfile", "stages": 3},
{"path": "./assets/dockerfiles/with-chunks.dockerfile", "stages": 3},
]
# TODO: Make tests for regex
def test_dockerfile_parser(docker_file_arrays):
for docker_file_info in docker_file_arrays:
docker_path = docker_file_info["path"]
actual_stages = docker_file_info["stages"]
docker = DockerFileParser.dockerfile_factory(Path(docker_path))
assert len(docker.stages) == actual_stages