janus-sec 0.1.0__tar.gz
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.
- janus_sec-0.1.0/.github/workflows/tests.yml +30 -0
- janus_sec-0.1.0/.gitignore +218 -0
- janus_sec-0.1.0/LICENSE +21 -0
- janus_sec-0.1.0/PKG-INFO +168 -0
- janus_sec-0.1.0/README.md +156 -0
- janus_sec-0.1.0/janus_sec/__init__.py +0 -0
- janus_sec-0.1.0/janus_sec/audit.py +84 -0
- janus_sec-0.1.0/janus_sec/checks/__init__.py +0 -0
- janus_sec-0.1.0/janus_sec/checks/allowlist.toml +16 -0
- janus_sec-0.1.0/janus_sec/checks/context.py +54 -0
- janus_sec-0.1.0/janus_sec/checks/group_ownership.py +118 -0
- janus_sec-0.1.0/janus_sec/checks/identity.py +38 -0
- janus_sec-0.1.0/janus_sec/checks/ownership.py +47 -0
- janus_sec-0.1.0/janus_sec/checks/symlinks.py +71 -0
- janus_sec-0.1.0/janus_sec/checks/world_permissions.py +75 -0
- janus_sec-0.1.0/janus_sec/cli.py +184 -0
- janus_sec-0.1.0/janus_sec/config.py +87 -0
- janus_sec-0.1.0/janus_sec/fix.py +94 -0
- janus_sec-0.1.0/janus_sec/models.py +79 -0
- janus_sec-0.1.0/janus_sec/platform_detect.py +79 -0
- janus_sec-0.1.0/janus_sec/scanner.py +132 -0
- janus_sec-0.1.0/janus_sec/targets.py +66 -0
- janus_sec-0.1.0/janus_sec/tui.py +201 -0
- janus_sec-0.1.0/pyproject.toml +28 -0
- janus_sec-0.1.0/tests/__init__.py +0 -0
- janus_sec-0.1.0/tests/test_audit.py +68 -0
- janus_sec-0.1.0/tests/test_cli.py +268 -0
- janus_sec-0.1.0/tests/test_config.py +160 -0
- janus_sec-0.1.0/tests/test_context.py +64 -0
- janus_sec-0.1.0/tests/test_fix.py +105 -0
- janus_sec-0.1.0/tests/test_group_ownership.py +105 -0
- janus_sec-0.1.0/tests/test_ownership.py +37 -0
- janus_sec-0.1.0/tests/test_platform_detect.py +60 -0
- janus_sec-0.1.0/tests/test_scanner.py +119 -0
- janus_sec-0.1.0/tests/test_symlinks.py +66 -0
- janus_sec-0.1.0/tests/test_targets.py +58 -0
- janus_sec-0.1.0/tests/test_tui.py +174 -0
- janus_sec-0.1.0/tests/test_world_permissions.py +75 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
strategy:
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12"]
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
|
|
19
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
20
|
+
uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
|
|
24
|
+
- name: Install package and test dependencies
|
|
25
|
+
run: |
|
|
26
|
+
pip install -e .
|
|
27
|
+
pip install pytest pytest-asyncio
|
|
28
|
+
|
|
29
|
+
- name: Run tests
|
|
30
|
+
run: pytest -v
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
# .idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
# Temporary file for partial code execution
|
|
204
|
+
tempCodeRunnerFile.py
|
|
205
|
+
|
|
206
|
+
# Ruff stuff:
|
|
207
|
+
.ruff_cache/
|
|
208
|
+
|
|
209
|
+
# PyPI configuration file
|
|
210
|
+
.pypirc
|
|
211
|
+
|
|
212
|
+
# Marimo
|
|
213
|
+
marimo/_static/
|
|
214
|
+
marimo/_lsp/
|
|
215
|
+
__marimo__/
|
|
216
|
+
|
|
217
|
+
# Streamlit
|
|
218
|
+
.streamlit/secrets.toml
|
janus_sec-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 A Sivasubramanian Manoj
|
|
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.
|
janus_sec-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: janus-sec
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A terminal-first credential exposure auditor
|
|
5
|
+
License: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: textual>=0.58
|
|
9
|
+
Requires-Dist: tomli-w>=1.0
|
|
10
|
+
Requires-Dist: tomli>=2.0; python_version < '3.11'
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# janus-sec
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
**A terminal-first credential exposure auditor.**
|
|
18
|
+
|
|
19
|
+
Your SSH keys, AWS credentials, and kube config are only as safe as their file
|
|
20
|
+
permissions. A `chmod 644` typo, a bad tarball extraction, or a cloud sync
|
|
21
|
+
tool can quietly leave a private key readable by every user on your machine
|
|
22
|
+
— and there's usually no warning until something goes wrong.
|
|
23
|
+
|
|
24
|
+
`janus-sec` scans the credential files you already have, tells you exactly
|
|
25
|
+
what's wrong and why it matters, and fixes it with one confirmed keystroke.
|
|
26
|
+
Nothing leaves your machine — no network calls, no file content is ever
|
|
27
|
+
read, no `sudo`.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
$ janus-sec
|
|
31
|
+
Scanned 4 file(s), 8 not present.
|
|
32
|
+
|
|
33
|
+
[HIGH] /home/user/.ssh/id_rsa
|
|
34
|
+
Issue: world_readable (mode 644)
|
|
35
|
+
Why: This file is readable by any local user on this machine.
|
|
36
|
+
Credential files should only be readable by their owner.
|
|
37
|
+
Fix: chmod 600 /home/user/.ssh/id_rsa
|
|
38
|
+
|
|
39
|
+
[MEDIUM] /home/user/.aws/credentials
|
|
40
|
+
Issue: group_readable (mode 640)
|
|
41
|
+
Why: This file is readable by group 'docker', which is not your
|
|
42
|
+
primary group. Other accounts in that group can read this file.
|
|
43
|
+
Fix: chmod 600 /home/user/.aws/credentials
|
|
44
|
+
|
|
45
|
+
2 finding(s) total, 1 HIGH risk.
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Why this exists
|
|
49
|
+
|
|
50
|
+
The underlying check here is genuinely simple — `find ~/.ssh -perm /077`
|
|
51
|
+
does most of the mechanical work in one line. That's not the point.
|
|
52
|
+
The gap `janus-sec` closes isn't "how do I check this," it's "I never
|
|
53
|
+
thought to check this, and if I did, I wouldn't trust a one-off command
|
|
54
|
+
to tell me what to do about it or keep a record that I did it."
|
|
55
|
+
|
|
56
|
+
Concretely, what a one-liner doesn't give you:
|
|
57
|
+
- **An explanation.** `-rw-r--r--` means nothing to most people at a
|
|
58
|
+
glance. "This private key is readable by any local user on this
|
|
59
|
+
machine" changes behavior.
|
|
60
|
+
- **The right fix for each file type.** SSH keys, `known_hosts`, AWS
|
|
61
|
+
credentials, and kube configs don't all want the same target mode.
|
|
62
|
+
- **A safe way to apply it.** Confirmation by default, `--dry-run` to
|
|
63
|
+
preview, and a re-check immediately before writing (the file you
|
|
64
|
+
looked at when scanning might not be the file you're about to change).
|
|
65
|
+
- **A record.** An append-only audit log of every fix, so "what did I
|
|
66
|
+
change and when" has a real answer.
|
|
67
|
+
- **A CI gate.** `--ci` exits non-zero on any HIGH-risk finding, so this
|
|
68
|
+
can fail a pipeline, not just print a warning nobody reads.
|
|
69
|
+
|
|
70
|
+
## What it checks
|
|
71
|
+
|
|
72
|
+
| Check | Risk | Auto-fix? |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| World-readable / world-writable | HIGH | Yes |
|
|
75
|
+
| Owned by a different user | HIGH | No — would require `sudo`, out of scope |
|
|
76
|
+
| Group-readable (non-primary group) | MEDIUM | Yes |
|
|
77
|
+
| Symlink pointing outside its expected directory | MEDIUM | No — target choice is a judgment call |
|
|
78
|
+
|
|
79
|
+
Scans by default: `~/.ssh/*`, `~/.aws/*`, `~/.kube/config`, `~/.npmrc`,
|
|
80
|
+
`~/.git-credentials`, `~/.docker/config.json`. Missing files are silently
|
|
81
|
+
skipped, not errors — most people won't have all of these.
|
|
82
|
+
|
|
83
|
+
## Safety properties
|
|
84
|
+
|
|
85
|
+
- **No network access, ever.**
|
|
86
|
+
- **Never reads file contents** — only metadata (permissions, ownership).
|
|
87
|
+
It can't tell you if a secret has leaked; it tells you if the file is
|
|
88
|
+
more exposed than it should be.
|
|
89
|
+
- **Never elevates privileges.** No `sudo`, no `chown`. If a fix would
|
|
90
|
+
require that, `janus-sec` reports the problem and stops — it doesn't
|
|
91
|
+
attempt it.
|
|
92
|
+
- **Every fix requires confirmation** (or an explicit `--yes` for
|
|
93
|
+
scripted use), and `--dry-run` previews changes with zero side effects.
|
|
94
|
+
- **Append-only audit log** of every fix applied, at
|
|
95
|
+
`~/.local/state/janus-sec/audit.log`.
|
|
96
|
+
|
|
97
|
+
## Install
|
|
98
|
+
|
|
99
|
+
Not yet published to PyPI — install from source for now:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
git clone https://github.com/A-S-Manoj/janus-sec.git
|
|
103
|
+
cd janus-sec
|
|
104
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
105
|
+
pip install -e .
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Usage
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Scan (also the default with no arguments - read-only, safe to run any time)
|
|
112
|
+
janus-sec
|
|
113
|
+
janus-sec scan
|
|
114
|
+
|
|
115
|
+
# Interactive TUI
|
|
116
|
+
janus-sec tui
|
|
117
|
+
|
|
118
|
+
# Machine-readable output
|
|
119
|
+
janus-sec scan --format json
|
|
120
|
+
|
|
121
|
+
# CI mode: exit 1 if any HIGH-risk finding exists
|
|
122
|
+
janus-sec scan --ci
|
|
123
|
+
|
|
124
|
+
# Fix everything fixable, with confirmation
|
|
125
|
+
janus-sec fix
|
|
126
|
+
|
|
127
|
+
# Fix one specific file
|
|
128
|
+
janus-sec fix ~/.ssh/id_rsa
|
|
129
|
+
|
|
130
|
+
# Preview fixes without changing anything
|
|
131
|
+
janus-sec fix --dry-run
|
|
132
|
+
|
|
133
|
+
# Skip the confirmation prompt (for scripts)
|
|
134
|
+
janus-sec fix --yes
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Configuration
|
|
138
|
+
|
|
139
|
+
`~/.config/janus-sec/config.toml` (optional — everything works with no
|
|
140
|
+
config file at all):
|
|
141
|
+
|
|
142
|
+
```toml
|
|
143
|
+
# Suppress a specific check on a specific file - not the whole file,
|
|
144
|
+
# just that one issue, so other real problems on it still get caught.
|
|
145
|
+
[[ignore]]
|
|
146
|
+
path = "/home/user/.ssh/known_hosts"
|
|
147
|
+
check_type = "group_readable"
|
|
148
|
+
note = "shared dev box, group access is intentional"
|
|
149
|
+
|
|
150
|
+
# Treat a group as known-safe, machine-wide.
|
|
151
|
+
[[allowlist]]
|
|
152
|
+
group = "wheel"
|
|
153
|
+
action = "suppress" # or "downgrade_to_low"
|
|
154
|
+
note = "trusted admin group on my machines"
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
## Development
|
|
158
|
+
|
|
159
|
+
Same setup as above, plus test dependencies:
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
pip install pytest pytest-asyncio
|
|
163
|
+
pytest -v
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
## License
|
|
167
|
+
|
|
168
|
+
MIT
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# janus-sec
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
**A terminal-first credential exposure auditor.**
|
|
6
|
+
|
|
7
|
+
Your SSH keys, AWS credentials, and kube config are only as safe as their file
|
|
8
|
+
permissions. A `chmod 644` typo, a bad tarball extraction, or a cloud sync
|
|
9
|
+
tool can quietly leave a private key readable by every user on your machine
|
|
10
|
+
— and there's usually no warning until something goes wrong.
|
|
11
|
+
|
|
12
|
+
`janus-sec` scans the credential files you already have, tells you exactly
|
|
13
|
+
what's wrong and why it matters, and fixes it with one confirmed keystroke.
|
|
14
|
+
Nothing leaves your machine — no network calls, no file content is ever
|
|
15
|
+
read, no `sudo`.
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
$ janus-sec
|
|
19
|
+
Scanned 4 file(s), 8 not present.
|
|
20
|
+
|
|
21
|
+
[HIGH] /home/user/.ssh/id_rsa
|
|
22
|
+
Issue: world_readable (mode 644)
|
|
23
|
+
Why: This file is readable by any local user on this machine.
|
|
24
|
+
Credential files should only be readable by their owner.
|
|
25
|
+
Fix: chmod 600 /home/user/.ssh/id_rsa
|
|
26
|
+
|
|
27
|
+
[MEDIUM] /home/user/.aws/credentials
|
|
28
|
+
Issue: group_readable (mode 640)
|
|
29
|
+
Why: This file is readable by group 'docker', which is not your
|
|
30
|
+
primary group. Other accounts in that group can read this file.
|
|
31
|
+
Fix: chmod 600 /home/user/.aws/credentials
|
|
32
|
+
|
|
33
|
+
2 finding(s) total, 1 HIGH risk.
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Why this exists
|
|
37
|
+
|
|
38
|
+
The underlying check here is genuinely simple — `find ~/.ssh -perm /077`
|
|
39
|
+
does most of the mechanical work in one line. That's not the point.
|
|
40
|
+
The gap `janus-sec` closes isn't "how do I check this," it's "I never
|
|
41
|
+
thought to check this, and if I did, I wouldn't trust a one-off command
|
|
42
|
+
to tell me what to do about it or keep a record that I did it."
|
|
43
|
+
|
|
44
|
+
Concretely, what a one-liner doesn't give you:
|
|
45
|
+
- **An explanation.** `-rw-r--r--` means nothing to most people at a
|
|
46
|
+
glance. "This private key is readable by any local user on this
|
|
47
|
+
machine" changes behavior.
|
|
48
|
+
- **The right fix for each file type.** SSH keys, `known_hosts`, AWS
|
|
49
|
+
credentials, and kube configs don't all want the same target mode.
|
|
50
|
+
- **A safe way to apply it.** Confirmation by default, `--dry-run` to
|
|
51
|
+
preview, and a re-check immediately before writing (the file you
|
|
52
|
+
looked at when scanning might not be the file you're about to change).
|
|
53
|
+
- **A record.** An append-only audit log of every fix, so "what did I
|
|
54
|
+
change and when" has a real answer.
|
|
55
|
+
- **A CI gate.** `--ci` exits non-zero on any HIGH-risk finding, so this
|
|
56
|
+
can fail a pipeline, not just print a warning nobody reads.
|
|
57
|
+
|
|
58
|
+
## What it checks
|
|
59
|
+
|
|
60
|
+
| Check | Risk | Auto-fix? |
|
|
61
|
+
|---|---|---|
|
|
62
|
+
| World-readable / world-writable | HIGH | Yes |
|
|
63
|
+
| Owned by a different user | HIGH | No — would require `sudo`, out of scope |
|
|
64
|
+
| Group-readable (non-primary group) | MEDIUM | Yes |
|
|
65
|
+
| Symlink pointing outside its expected directory | MEDIUM | No — target choice is a judgment call |
|
|
66
|
+
|
|
67
|
+
Scans by default: `~/.ssh/*`, `~/.aws/*`, `~/.kube/config`, `~/.npmrc`,
|
|
68
|
+
`~/.git-credentials`, `~/.docker/config.json`. Missing files are silently
|
|
69
|
+
skipped, not errors — most people won't have all of these.
|
|
70
|
+
|
|
71
|
+
## Safety properties
|
|
72
|
+
|
|
73
|
+
- **No network access, ever.**
|
|
74
|
+
- **Never reads file contents** — only metadata (permissions, ownership).
|
|
75
|
+
It can't tell you if a secret has leaked; it tells you if the file is
|
|
76
|
+
more exposed than it should be.
|
|
77
|
+
- **Never elevates privileges.** No `sudo`, no `chown`. If a fix would
|
|
78
|
+
require that, `janus-sec` reports the problem and stops — it doesn't
|
|
79
|
+
attempt it.
|
|
80
|
+
- **Every fix requires confirmation** (or an explicit `--yes` for
|
|
81
|
+
scripted use), and `--dry-run` previews changes with zero side effects.
|
|
82
|
+
- **Append-only audit log** of every fix applied, at
|
|
83
|
+
`~/.local/state/janus-sec/audit.log`.
|
|
84
|
+
|
|
85
|
+
## Install
|
|
86
|
+
|
|
87
|
+
Not yet published to PyPI — install from source for now:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
git clone https://github.com/A-S-Manoj/janus-sec.git
|
|
91
|
+
cd janus-sec
|
|
92
|
+
python3 -m venv .venv && source .venv/bin/activate
|
|
93
|
+
pip install -e .
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Usage
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
# Scan (also the default with no arguments - read-only, safe to run any time)
|
|
100
|
+
janus-sec
|
|
101
|
+
janus-sec scan
|
|
102
|
+
|
|
103
|
+
# Interactive TUI
|
|
104
|
+
janus-sec tui
|
|
105
|
+
|
|
106
|
+
# Machine-readable output
|
|
107
|
+
janus-sec scan --format json
|
|
108
|
+
|
|
109
|
+
# CI mode: exit 1 if any HIGH-risk finding exists
|
|
110
|
+
janus-sec scan --ci
|
|
111
|
+
|
|
112
|
+
# Fix everything fixable, with confirmation
|
|
113
|
+
janus-sec fix
|
|
114
|
+
|
|
115
|
+
# Fix one specific file
|
|
116
|
+
janus-sec fix ~/.ssh/id_rsa
|
|
117
|
+
|
|
118
|
+
# Preview fixes without changing anything
|
|
119
|
+
janus-sec fix --dry-run
|
|
120
|
+
|
|
121
|
+
# Skip the confirmation prompt (for scripts)
|
|
122
|
+
janus-sec fix --yes
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Configuration
|
|
126
|
+
|
|
127
|
+
`~/.config/janus-sec/config.toml` (optional — everything works with no
|
|
128
|
+
config file at all):
|
|
129
|
+
|
|
130
|
+
```toml
|
|
131
|
+
# Suppress a specific check on a specific file - not the whole file,
|
|
132
|
+
# just that one issue, so other real problems on it still get caught.
|
|
133
|
+
[[ignore]]
|
|
134
|
+
path = "/home/user/.ssh/known_hosts"
|
|
135
|
+
check_type = "group_readable"
|
|
136
|
+
note = "shared dev box, group access is intentional"
|
|
137
|
+
|
|
138
|
+
# Treat a group as known-safe, machine-wide.
|
|
139
|
+
[[allowlist]]
|
|
140
|
+
group = "wheel"
|
|
141
|
+
action = "suppress" # or "downgrade_to_low"
|
|
142
|
+
note = "trusted admin group on my machines"
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Development
|
|
146
|
+
|
|
147
|
+
Same setup as above, plus test dependencies:
|
|
148
|
+
|
|
149
|
+
```bash
|
|
150
|
+
pip install pytest pytest-asyncio
|
|
151
|
+
pytest -v
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
File without changes
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Append-only audit log of every fix applied.
|
|
2
|
+
|
|
3
|
+
Deliberately plain text, one line per entry - meant to be human-readable
|
|
4
|
+
(tail -f it, grep it) rather than a structured format, since machine-
|
|
5
|
+
readable output already exists separately via --format json on scan.
|
|
6
|
+
|
|
7
|
+
Never logs file CONTENTS, only the operation metadata: what path, what
|
|
8
|
+
mode changed to what, when, and how (cli vs tui).
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
from datetime import datetime, timezone
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@dataclass(frozen=True, slots=True)
|
|
20
|
+
class AuditLogEntry:
|
|
21
|
+
timestamp: str # ISO 8601 UTC
|
|
22
|
+
path: str
|
|
23
|
+
before_mode_octal: str
|
|
24
|
+
after_mode_octal: str
|
|
25
|
+
applied_via: str # "cli" | "tui"
|
|
26
|
+
|
|
27
|
+
def to_line(self) -> str:
|
|
28
|
+
return (
|
|
29
|
+
f"{self.timestamp} chmod {self.path} "
|
|
30
|
+
f"{self.before_mode_octal} -> {self.after_mode_octal} "
|
|
31
|
+
f"(via {self.applied_via})"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def default_log_path() -> Path:
|
|
36
|
+
# XDG state directory - the conventional home for append-only logs
|
|
37
|
+
# and other "history of what happened" data, distinct from config
|
|
38
|
+
# (XDG_CONFIG_HOME) and cache (XDG_CACHE_HOME).
|
|
39
|
+
xdg_state = os.environ.get("XDG_STATE_HOME")
|
|
40
|
+
base = Path(xdg_state) if xdg_state else Path.home() / ".local" / "state"
|
|
41
|
+
return base / "janus-sec" / "audit.log"
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def append_entry(
|
|
45
|
+
path: str,
|
|
46
|
+
before_mode_octal: str,
|
|
47
|
+
after_mode_octal: str,
|
|
48
|
+
applied_via: str,
|
|
49
|
+
log_path: Path | None = None,
|
|
50
|
+
) -> AuditLogEntry:
|
|
51
|
+
"""Append one entry to the audit log, creating the log file/directory
|
|
52
|
+
if this is the first entry ever written.
|
|
53
|
+
"""
|
|
54
|
+
if log_path is None:
|
|
55
|
+
log_path = default_log_path()
|
|
56
|
+
|
|
57
|
+
entry = AuditLogEntry(
|
|
58
|
+
timestamp=datetime.now(timezone.utc).isoformat(timespec="seconds"),
|
|
59
|
+
path=path,
|
|
60
|
+
before_mode_octal=before_mode_octal,
|
|
61
|
+
after_mode_octal=after_mode_octal,
|
|
62
|
+
applied_via=applied_via,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
66
|
+
with open(log_path, "a", encoding="utf-8") as f:
|
|
67
|
+
f.write(entry.to_line() + "\n")
|
|
68
|
+
|
|
69
|
+
return entry
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def read_entries(log_path: Path | None = None) -> list[str]:
|
|
73
|
+
"""Read raw log lines, most recent last (natural file order).
|
|
74
|
+
Returns an empty list if the log doesn't exist yet - no entries
|
|
75
|
+
logged is not an error.
|
|
76
|
+
"""
|
|
77
|
+
if log_path is None:
|
|
78
|
+
log_path = default_log_path()
|
|
79
|
+
|
|
80
|
+
if not log_path.exists():
|
|
81
|
+
return []
|
|
82
|
+
|
|
83
|
+
with open(log_path, encoding="utf-8") as f:
|
|
84
|
+
return [line.rstrip("\n") for line in f if line.strip()]
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Known-benign group-ownership patterns.
|
|
2
|
+
#
|
|
3
|
+
# Each [[pattern]] suppresses (or downgrades) a group-readable finding
|
|
4
|
+
# when the file's group matches `group` (and, if given, the OS matches
|
|
5
|
+
# `os`). Left empty for now - only add entries once verified against
|
|
6
|
+
# real machines, not speculatively.
|
|
7
|
+
#
|
|
8
|
+
# Example of the shape an entry will take, once verified:
|
|
9
|
+
#
|
|
10
|
+
# [[pattern]]
|
|
11
|
+
# os = "linux"
|
|
12
|
+
# group = "wheel"
|
|
13
|
+
# action = "suppress" # "suppress" or "downgrade_to_low"
|
|
14
|
+
# note = "why this is considered safe"
|
|
15
|
+
|
|
16
|
+
patterns = []
|