appsec 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- appsec/__init__.py +11 -0
- appsec/__main__.py +8 -0
- appsec/cli/__init__.py +105 -0
- appsec/cli/_commands/__init__.py +0 -0
- appsec/cli/_commands/explain.py +40 -0
- appsec/cli/_commands/learn.py +44 -0
- appsec/cli/_commands/whoami.py +40 -0
- appsec/cli/_errors.py +39 -0
- appsec/cli/_output.py +45 -0
- appsec-0.1.0.dist-info/METADATA +20 -0
- appsec-0.1.0.dist-info/RECORD +14 -0
- appsec-0.1.0.dist-info/WHEEL +4 -0
- appsec-0.1.0.dist-info/entry_points.txt +2 -0
- appsec-0.1.0.dist-info/licenses/LICENSE +21 -0
appsec/__init__.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""appsec — an application-security agent (greenfield AgentCulture sibling)."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError
|
|
4
|
+
from importlib.metadata import version as _v
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
__version__ = _v("appsec")
|
|
8
|
+
except PackageNotFoundError: # pragma: no cover — editable install without metadata
|
|
9
|
+
__version__ = "0.0.0+local"
|
|
10
|
+
|
|
11
|
+
__all__ = ["__version__"]
|
appsec/__main__.py
ADDED
appsec/cli/__init__.py
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""Unified CLI entry point for appsec.
|
|
2
|
+
|
|
3
|
+
Error-propagation contract: every handler raises
|
|
4
|
+
:class:`appsec.cli._errors.AppsecError` on failure; ``main()`` catches it via
|
|
5
|
+
:func:`_dispatch` and routes through :mod:`appsec.cli._output`. Unknown
|
|
6
|
+
exceptions are wrapped into an ``AppsecError`` so no Python traceback leaks.
|
|
7
|
+
|
|
8
|
+
Argparse errors (unknown verb, missing required arg) also route through the
|
|
9
|
+
structured format — :class:`_AppsecArgumentParser` overrides ``.error()``.
|
|
10
|
+
Whether errors render as text or JSON depends on whether ``--json`` appears in
|
|
11
|
+
the raw argv (:func:`main` sets ``_AppsecArgumentParser._json_hint`` before
|
|
12
|
+
``parse_args``).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import sys
|
|
19
|
+
|
|
20
|
+
from appsec import __version__
|
|
21
|
+
from appsec.cli._errors import EXIT_USER_ERROR, AppsecError
|
|
22
|
+
from appsec.cli._output import emit_error
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _AppsecArgumentParser(argparse.ArgumentParser):
|
|
26
|
+
"""ArgumentParser that routes errors through :func:`emit_error`."""
|
|
27
|
+
|
|
28
|
+
_json_hint: bool = False
|
|
29
|
+
|
|
30
|
+
def error(self, message: str) -> None: # type: ignore[override]
|
|
31
|
+
err = AppsecError(
|
|
32
|
+
code=EXIT_USER_ERROR,
|
|
33
|
+
message=message,
|
|
34
|
+
remediation=f"run '{self.prog} --help' to see valid arguments",
|
|
35
|
+
)
|
|
36
|
+
emit_error(err, json_mode=type(self)._json_hint)
|
|
37
|
+
raise SystemExit(err.code)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _argv_has_json(argv: list[str] | None) -> bool:
|
|
41
|
+
tokens = argv if argv is not None else sys.argv[1:]
|
|
42
|
+
return any(t == "--json" or t.startswith("--json=") for t in tokens)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
46
|
+
parser = _AppsecArgumentParser(
|
|
47
|
+
prog="appsec",
|
|
48
|
+
description="appsec — an application-security agent (greenfield).",
|
|
49
|
+
)
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
"--version",
|
|
52
|
+
action="version",
|
|
53
|
+
version=f"%(prog)s {__version__}",
|
|
54
|
+
)
|
|
55
|
+
sub = parser.add_subparsers(dest="command", parser_class=_AppsecArgumentParser)
|
|
56
|
+
|
|
57
|
+
from appsec.cli._commands import explain as _explain_cmd
|
|
58
|
+
from appsec.cli._commands import learn as _learn_cmd
|
|
59
|
+
from appsec.cli._commands import whoami as _whoami_cmd
|
|
60
|
+
|
|
61
|
+
_learn_cmd.register(sub)
|
|
62
|
+
_explain_cmd.register(sub)
|
|
63
|
+
_whoami_cmd.register(sub)
|
|
64
|
+
|
|
65
|
+
return parser
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _dispatch(args: argparse.Namespace) -> int:
|
|
69
|
+
"""Invoke the registered handler and translate exceptions to exit codes.
|
|
70
|
+
|
|
71
|
+
A handler may return ``None`` (treated as success, exit 0) or an ``int``
|
|
72
|
+
used directly as the exit code. Failures MUST raise :class:`AppsecError`;
|
|
73
|
+
any other exception is wrapped so no Python traceback leaks.
|
|
74
|
+
"""
|
|
75
|
+
json_mode = bool(getattr(args, "json", False))
|
|
76
|
+
try:
|
|
77
|
+
rc = args.func(args)
|
|
78
|
+
except AppsecError as err:
|
|
79
|
+
emit_error(err, json_mode=json_mode)
|
|
80
|
+
return err.code
|
|
81
|
+
except Exception as err: # noqa: BLE001 - last-resort; wrap and route cleanly
|
|
82
|
+
wrapped = AppsecError(
|
|
83
|
+
code=EXIT_USER_ERROR,
|
|
84
|
+
message=f"unexpected: {err.__class__.__name__}: {err}",
|
|
85
|
+
remediation="file a bug at https://github.com/agentculture/appsec/issues",
|
|
86
|
+
)
|
|
87
|
+
emit_error(wrapped, json_mode=json_mode)
|
|
88
|
+
return wrapped.code
|
|
89
|
+
return rc if rc is not None else 0
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def main(argv: list[str] | None = None) -> int:
|
|
93
|
+
_AppsecArgumentParser._json_hint = _argv_has_json(argv)
|
|
94
|
+
parser = _build_parser()
|
|
95
|
+
args = parser.parse_args(argv)
|
|
96
|
+
|
|
97
|
+
if args.command is None:
|
|
98
|
+
parser.print_help()
|
|
99
|
+
return 0
|
|
100
|
+
|
|
101
|
+
return _dispatch(args)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
if __name__ == "__main__":
|
|
105
|
+
sys.exit(main())
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""``appsec explain`` — placeholder verb.
|
|
2
|
+
|
|
3
|
+
See :mod:`appsec.cli._commands.learn` for why the verbs are stubs. ``explain``
|
|
4
|
+
will eventually print docs for a given topic / command path; today it prints
|
|
5
|
+
an honest "not yet implemented" line.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
|
|
12
|
+
from appsec import __version__
|
|
13
|
+
from appsec.cli._output import emit_result
|
|
14
|
+
|
|
15
|
+
_TEXT = "appsec explain — not yet implemented; appsec is greenfield. See CLAUDE.md."
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _json_payload() -> dict[str, object]:
|
|
19
|
+
return {
|
|
20
|
+
"tool": "appsec",
|
|
21
|
+
"version": __version__,
|
|
22
|
+
"status": "greenfield",
|
|
23
|
+
"verb": "explain",
|
|
24
|
+
"message": _TEXT,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def cmd_explain(args: argparse.Namespace) -> int:
|
|
29
|
+
json_mode = bool(getattr(args, "json", False))
|
|
30
|
+
if json_mode:
|
|
31
|
+
emit_result(_json_payload(), json_mode=True)
|
|
32
|
+
else:
|
|
33
|
+
emit_result(_TEXT, json_mode=False)
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
38
|
+
p = sub.add_parser("explain", help="Explain an appsec topic or command (stub).")
|
|
39
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
40
|
+
p.set_defaults(func=cmd_explain)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""``appsec learn`` — placeholder verb.
|
|
2
|
+
|
|
3
|
+
appsec is a greenfield AgentCulture sibling: the scaffold (package, CLI
|
|
4
|
+
chassis, CI, vendored skills) is in place but the application-security agent
|
|
5
|
+
itself is not implemented yet. This verb prints an honest status line so a
|
|
6
|
+
probing agent or human gets a clear signal rather than a misleading response.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
|
|
13
|
+
from appsec import __version__
|
|
14
|
+
from appsec.cli._output import emit_result
|
|
15
|
+
|
|
16
|
+
_TEXT = (
|
|
17
|
+
"appsec — application-security agent. Not yet implemented; appsec is a "
|
|
18
|
+
"greenfield AgentCulture sibling. See CLAUDE.md."
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _json_payload() -> dict[str, object]:
|
|
23
|
+
return {
|
|
24
|
+
"tool": "appsec",
|
|
25
|
+
"version": __version__,
|
|
26
|
+
"status": "greenfield",
|
|
27
|
+
"verb": "learn",
|
|
28
|
+
"message": _TEXT,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def cmd_learn(args: argparse.Namespace) -> int:
|
|
33
|
+
json_mode = bool(getattr(args, "json", False))
|
|
34
|
+
if json_mode:
|
|
35
|
+
emit_result(_json_payload(), json_mode=True)
|
|
36
|
+
else:
|
|
37
|
+
emit_result(_TEXT, json_mode=False)
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
42
|
+
p = sub.add_parser("learn", help="Print appsec's self-teaching status line.")
|
|
43
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
44
|
+
p.set_defaults(func=cmd_learn)
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""``appsec whoami`` — placeholder verb.
|
|
2
|
+
|
|
3
|
+
See :mod:`appsec.cli._commands.learn` for why the verbs are stubs. ``whoami``
|
|
4
|
+
will eventually be the smallest identity / auth probe; today it prints an
|
|
5
|
+
honest "not yet implemented" line.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
|
|
12
|
+
from appsec import __version__
|
|
13
|
+
from appsec.cli._output import emit_result
|
|
14
|
+
|
|
15
|
+
_TEXT = "appsec — not yet implemented; appsec is greenfield. See CLAUDE.md."
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _json_payload() -> dict[str, object]:
|
|
19
|
+
return {
|
|
20
|
+
"tool": "appsec",
|
|
21
|
+
"version": __version__,
|
|
22
|
+
"status": "greenfield",
|
|
23
|
+
"verb": "whoami",
|
|
24
|
+
"message": _TEXT,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def cmd_whoami(args: argparse.Namespace) -> int:
|
|
29
|
+
json_mode = bool(getattr(args, "json", False))
|
|
30
|
+
if json_mode:
|
|
31
|
+
emit_result(_json_payload(), json_mode=True)
|
|
32
|
+
else:
|
|
33
|
+
emit_result(_TEXT, json_mode=False)
|
|
34
|
+
return 0
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def register(sub: argparse._SubParsersAction) -> None:
|
|
38
|
+
p = sub.add_parser("whoami", help="Print appsec's identity probe (stub).")
|
|
39
|
+
p.add_argument("--json", action="store_true", help="Emit structured JSON.")
|
|
40
|
+
p.set_defaults(func=cmd_whoami)
|
appsec/cli/_errors.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"""AppsecError and exit-code policy.
|
|
2
|
+
|
|
3
|
+
Every failure inside appsec raises :class:`AppsecError`. The top-level
|
|
4
|
+
``main()`` catches it, formats via :mod:`appsec.cli._output`, and exits with
|
|
5
|
+
:attr:`AppsecError.code`. This centralises the exit-code policy and guarantees
|
|
6
|
+
no Python traceback leaks to stderr.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
|
|
13
|
+
# Exit-code policy:
|
|
14
|
+
# 0 = success
|
|
15
|
+
# 1 = user-input error (bad flag, missing required arg, unknown path)
|
|
16
|
+
# 2 = environment / setup error (tool not installed, file unreadable)
|
|
17
|
+
# 3+ = reserved for future categorisation
|
|
18
|
+
EXIT_SUCCESS = 0
|
|
19
|
+
EXIT_USER_ERROR = 1
|
|
20
|
+
EXIT_ENV_ERROR = 2
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class AppsecError(Exception):
|
|
25
|
+
"""Structured error raised within appsec; carries a remediation hint."""
|
|
26
|
+
|
|
27
|
+
code: int
|
|
28
|
+
message: str
|
|
29
|
+
remediation: str = ""
|
|
30
|
+
|
|
31
|
+
def __post_init__(self) -> None:
|
|
32
|
+
super().__init__(self.message)
|
|
33
|
+
|
|
34
|
+
def to_dict(self) -> dict[str, object]:
|
|
35
|
+
return {
|
|
36
|
+
"code": self.code,
|
|
37
|
+
"message": self.message,
|
|
38
|
+
"remediation": self.remediation,
|
|
39
|
+
}
|
appsec/cli/_output.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""stdout / stderr helpers with a strict split.
|
|
2
|
+
|
|
3
|
+
Rule: **results go to stdout, diagnostics and errors go to stderr.** Agents
|
|
4
|
+
parsing appsec output can rely on this invariant. JSON mode routes structured
|
|
5
|
+
payloads to the same streams — it never mixes them.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import sys
|
|
12
|
+
from typing import Any, TextIO
|
|
13
|
+
|
|
14
|
+
from appsec.cli._errors import AppsecError
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def emit_result(data: Any, *, json_mode: bool, stream: TextIO | None = None) -> None:
|
|
18
|
+
"""Write a command result to stdout (text or JSON), newline-terminated."""
|
|
19
|
+
s = stream if stream is not None else sys.stdout
|
|
20
|
+
if json_mode:
|
|
21
|
+
json.dump(data, s, ensure_ascii=False)
|
|
22
|
+
s.write("\n")
|
|
23
|
+
return
|
|
24
|
+
text = data if isinstance(data, str) else str(data)
|
|
25
|
+
s.write(text)
|
|
26
|
+
if not text.endswith("\n"):
|
|
27
|
+
s.write("\n")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def emit_error(err: AppsecError, *, json_mode: bool, stream: TextIO | None = None) -> None:
|
|
31
|
+
"""Write an :class:`AppsecError` to stderr (text or JSON)."""
|
|
32
|
+
s = stream if stream is not None else sys.stderr
|
|
33
|
+
if json_mode:
|
|
34
|
+
json.dump(err.to_dict(), s, ensure_ascii=False)
|
|
35
|
+
s.write("\n")
|
|
36
|
+
return
|
|
37
|
+
s.write(f"error: {err.message}\n")
|
|
38
|
+
if err.remediation:
|
|
39
|
+
s.write(f"hint: {err.remediation}\n")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def emit_diagnostic(message: str, *, stream: TextIO | None = None) -> None:
|
|
43
|
+
"""Write a human diagnostic (progress, summary) to stderr."""
|
|
44
|
+
s = stream if stream is not None else sys.stderr
|
|
45
|
+
s.write(message if message.endswith("\n") else message + "\n")
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: appsec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: appsec — an application-security agent (greenfield AgentCulture sibling).
|
|
5
|
+
Project-URL: Homepage, https://github.com/agentculture/appsec
|
|
6
|
+
Project-URL: Issues, https://github.com/agentculture/appsec/issues
|
|
7
|
+
Author: AgentCulture
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Topic :: Software Development
|
|
15
|
+
Requires-Python: >=3.12
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
|
|
18
|
+
# appsec
|
|
19
|
+
|
|
20
|
+
An appsec agent
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
appsec/__init__.py,sha256=uHDR0tMi7OlgoyGHiKNhrXeg6Sk2ZYehs64wgr76wgY,364
|
|
2
|
+
appsec/__main__.py,sha256=qjWptLDZzLmK3YjzbzhgdJKdaZxuj_y4rWR_HXotJ5k,142
|
|
3
|
+
appsec/cli/__init__.py,sha256=bNp4BLlqyHu6-YzEOiDUHkHK-oqeoGJeVm_rj_y9x4g,3510
|
|
4
|
+
appsec/cli/_errors.py,sha256=pXt9Kz7yIX2lrudbxt6doQcAjgGE5Kh4UhCexkP_7oE,1101
|
|
5
|
+
appsec/cli/_output.py,sha256=H2dNXc1jsBvPId3SzeSQazEW5SFeu0YttZN4licSBGE,1543
|
|
6
|
+
appsec/cli/_commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
7
|
+
appsec/cli/_commands/explain.py,sha256=Avc91wx1rZU2HvkFQoEwQhDywXNPP1Cd3SnwJiGQ7AE,1165
|
|
8
|
+
appsec/cli/_commands/learn.py,sha256=GkqWMqFsBNPVaMtgShY2vw0qSOEZycdjop4_gmzlLbs,1320
|
|
9
|
+
appsec/cli/_commands/whoami.py,sha256=IIpcAJRvA28krghkBLbPQKMiXookUCTfeb1xvilPEaI,1140
|
|
10
|
+
appsec-0.1.0.dist-info/METADATA,sha256=jku8VnReCOYwWrM5Kt9ZBfsunM1bG8IAaxFmCpT0_Ng,654
|
|
11
|
+
appsec-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
12
|
+
appsec-0.1.0.dist-info/entry_points.txt,sha256=PvR0G1hKw0s9YAco6SB_pIuz4_gI6RouIatLASN-jcI,43
|
|
13
|
+
appsec-0.1.0.dist-info/licenses/LICENSE,sha256=wCcdPywGtFXx1P8N0j0eEDINSWfSjrIsU7ds1YZl-MA,1069
|
|
14
|
+
appsec-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 AgentCulture
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|