checkmyvibe 1.0.0
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.
- package/LICENSE +201 -0
- package/README.md +67 -0
- package/SKILL.md +326 -0
- package/install.js +89 -0
- package/package.json +31 -0
- package/references/vibe_vulnerability_patterns.md +81 -0
- package/scripts/check_auth_patterns.py +112 -0
- package/scripts/check_db_config.py +144 -0
- package/scripts/check_gitignore.py +139 -0
- package/scripts/scan_secrets.py +148 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
package/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# checkmyvibe
|
|
2
|
+
|
|
3
|
+
A structured, zero-dependency, local security-audit workflow packaged as an Agent Skill for AI coding assistants (such as Claude Code, Cursor, Codex, and Gemini CLI).
|
|
4
|
+
|
|
5
|
+
AI-generated ("vibe-coded") applications built on modern AI tools frequently ship with serious, well-documented security flaws—such as exposed API keys, fake authentication stubs, permissive default database rules, and client-side pricing logic. **checkmyvibe** solves this by packaging security checks directly into an Agent Skill. When you ask your coding agent to "run a security audit," the agent uses checkmyvibe's local helper scripts and its own reasoning capabilities to analyze your codebase, producing a prioritized report detailing what's wrong, why it matters, and how to fix it.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## What This Checks For
|
|
10
|
+
|
|
11
|
+
* **Exposed Secrets & API Keys:** Identifies hardcoded API keys, JWT secrets, database credentials, and high-entropy strings across your files (using standard prefixes like `sk_live`, `AIza`, `AKIA`, and `ghp_`).
|
|
12
|
+
* **Version Control Leakage:** Verifies if sensitive files (like `.env`) exist in the project but are not properly excluded in your `.gitignore` file.
|
|
13
|
+
* **Fake & Stubbed Authentication:** Greps for common mock authentication markers (such as functions named `mockAuth`, `fakeLogin`, `tempAuth`, or logic that returns `true` unconditionally to bypass authentication checks).
|
|
14
|
+
* **Database Misconfigurations:** Identifies permissive default rules (such as `allow read, write: if true;` in Firebase/Firestore configs) and checks if Row-Level Security (RLS) is enabled on Supabase database schemas.
|
|
15
|
+
* **Broken Object-Level Authorization (BOLA/IDOR):** Analyzes API routes to check if endpoints fetch resources by ID without validating that the authenticated user owns or has permission to access that resource.
|
|
16
|
+
* **Client-Side Payment & Pricing Logic:** Scans checkout routes and payment integrations to check if prices or transaction values are calculated or accepted directly from client-side parameters rather than securely fetched on the server.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## What This Does NOT Check For (Disclaimer)
|
|
21
|
+
|
|
22
|
+
> [!WARNING]
|
|
23
|
+
> **checkmyvibe is not a substitute for a full professional security audit.**
|
|
24
|
+
> This tool is a first-pass heuristic scanner meant to highlight common mistakes made by AI coding models during rapid scaffolding. It does not perform dynamic runtime analysis, penetration testing, deep static analysis, dependency vulnerability checks, or comprehensive logic auditing. Do not rely solely on checkmyvibe to declare your application secure for production.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
|
|
30
|
+
### Method 1: Using npx (Recommended)
|
|
31
|
+
You can install and copy the skill automatically to your project or global environment by running:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
npx checkmyvibe
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The installer will detect if you have a `.claude` configuration directory in your project root and offer to install it either locally (project-level) or globally for your user.
|
|
38
|
+
|
|
39
|
+
### Method 2: Manual Installation
|
|
40
|
+
If you prefer not to use `npx`, copy the files manually:
|
|
41
|
+
|
|
42
|
+
1. Create a `checkmyvibe` directory inside your agent's skills path:
|
|
43
|
+
* **Project-level (Claude Code):** `.claude/skills/checkmyvibe/`
|
|
44
|
+
* **Global/Personal (Claude Code):** `~/.claude/skills/checkmyvibe/`
|
|
45
|
+
2. Copy `SKILL.md`, the `scripts/` folder, and the `references/` folder into that directory.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
Once installed, your AI coding agent will automatically recognize the `checkmyvibe` skill when relevant. You can trigger the workflow explicitly:
|
|
52
|
+
|
|
53
|
+
### Claude Code
|
|
54
|
+
Open your terminal in the workspace and type:
|
|
55
|
+
```bash
|
|
56
|
+
/bug run a security audit
|
|
57
|
+
```
|
|
58
|
+
*Or simply ask Claude in the chat:*
|
|
59
|
+
> "Run a security audit on this project using checkmyvibe"
|
|
60
|
+
|
|
61
|
+
The agent will walk through the checks, execute the local python validation scripts, and print a prioritized markdown report (Critical / Should Fix / Worth Reviewing) with instructions on how to correct the issues.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## License
|
|
66
|
+
|
|
67
|
+
This project is open-source and licensed under the [Apache 2.0 License](LICENSE).
|
package/SKILL.md
ADDED
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: checkmyvibe
|
|
3
|
+
description: >
|
|
4
|
+
Use this skill when the user asks for a security review, security audit, penetration test, or "is this safe to ship / safe to launch" check. Also trigger proactively when the agent is about to help deploy, publish, or push an application live, or when the codebase shows signs of AI-scaffolded patterns (Supabase/Firebase config, recently generated boilerplate, auth stubs, no existing security review) and the user has not had one done yet. Covers the specific, documented vulnerability patterns common in AI-generated and vibe-coded applications: exposed secrets, missing or fake authentication, permissive database rules, broken object-level authorization, unvalidated inputs, and client-side payment or pricing logic.
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# checkmyvibe — Security Audit for Vibe-Coded Apps
|
|
8
|
+
|
|
9
|
+
## Why this skill exists
|
|
10
|
+
|
|
11
|
+
AI coding tools optimize for "it works," not "it's safe." A feature can pass every
|
|
12
|
+
manual test a non-technical founder runs — sign up, log in, place an order — and
|
|
13
|
+
still leak every other user's data to a stranger who changes one number in a URL.
|
|
14
|
+
This happens because AI-generated code frequently ships with scaffolding shortcuts
|
|
15
|
+
that were meant to be temporary (a stub auth check, a permissive default database
|
|
16
|
+
rule) and never get hardened before launch, precisely because the person building
|
|
17
|
+
the app doesn't know those shortcuts exist or what to look for.
|
|
18
|
+
|
|
19
|
+
Your job when this skill is active: think like a security engineer doing a
|
|
20
|
+
pre-launch review for a client who has never heard the words "IDOR" or "row-level
|
|
21
|
+
security." Find the real, exploitable issues. Explain them in plain language. Give
|
|
22
|
+
exact fixes. Do not pad the report with theoretical concerns that don't apply to
|
|
23
|
+
this specific codebase, and do not skip a check because the codebase "looks
|
|
24
|
+
simple" — simple codebases are exactly where stubbed auth and hardcoded secrets
|
|
25
|
+
hide, because nobody expected them to hold real user data yet.
|
|
26
|
+
|
|
27
|
+
## Scope and honesty (read this before writing any report)
|
|
28
|
+
|
|
29
|
+
This is a first-pass audit for known, documented failure patterns. It is not a
|
|
30
|
+
full penetration test, it does not cover infrastructure security, dependency
|
|
31
|
+
vulnerabilities, or novel/business-logic-specific flaws outside the six categories
|
|
32
|
+
below. Never tell the user their app is "secure" or "safe" in an unqualified way.
|
|
33
|
+
The correct language is "no issues found in this pass" or "ready to ship as far as
|
|
34
|
+
these checks go" — always paired with the scope reminder in the Final Summary
|
|
35
|
+
section. If the app appears to handle payments, health data, or other regulated
|
|
36
|
+
data, say explicitly that a professional audit is strongly recommended regardless
|
|
37
|
+
of what this pass finds.
|
|
38
|
+
|
|
39
|
+
## Before you start
|
|
40
|
+
|
|
41
|
+
1. **Analyze and understand the whole project first:** Walk the directory tree and analyze the repository configuration files before running any checks, generating findings, or providing instructions. Establish a solid high-level understanding of the architecture, components, and data flow.
|
|
42
|
+
2. Identify the stack: what backend/framework, what database or BaaS provider
|
|
43
|
+
(Supabase, Firebase, custom Postgres, etc.), what auth approach is in use.
|
|
44
|
+
This changes which checks apply and how to phrase findings.
|
|
45
|
+
3. Identify what data the app handles: user accounts, payments, health data,
|
|
46
|
+
messages between users, files. This changes severity judgments — the same
|
|
47
|
+
missing check is more severe on an app handling payment data than on a toy
|
|
48
|
+
to-do list.
|
|
49
|
+
4. Run the checks below in order. Use the bundled scripts where noted. For
|
|
50
|
+
everything else, read the actual code — don't rely on file names alone.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
## Supported stacks to recognize
|
|
56
|
+
|
|
57
|
+
Common stacks this skill should expect include: Next.js, Express, FastAPI, Supabase, Firebase, Prisma, Postgres, MongoDB, Clerk, NextAuth, Stripe, and common app patterns built around them.
|
|
58
|
+
|
|
59
|
+
## Evidence and confidence rules
|
|
60
|
+
|
|
61
|
+
- Only flag something when there is evidence in the code.
|
|
62
|
+
- Do not guess from filenames, comments, or variable names alone.
|
|
63
|
+
- If something looks suspicious but is not fully proven, mark it with a confidence tag such as `High confidence`, `Medium confidence`, or `Needs manual review`.
|
|
64
|
+
- Do not overflag. Prefer one precise finding over several weak or duplicate ones.
|
|
65
|
+
|
|
66
|
+
## Fix priority
|
|
67
|
+
|
|
68
|
+
When multiple issues are found, sort them in this order:
|
|
69
|
+
Critical user exposure > auth bypass > IDOR > payment logic > config issues > hygiene.
|
|
70
|
+
|
|
71
|
+
## What success looks like
|
|
72
|
+
|
|
73
|
+
The report should help the coding agent make the repo safer in the next commit, not just describe problems.
|
|
74
|
+
|
|
75
|
+
## Check 1: Exposed secrets and credentials
|
|
76
|
+
|
|
77
|
+
**Run the script** `scripts/scan_secrets.py` (located relative to this `SKILL.md` file) against the project root. It flags known key
|
|
78
|
+
prefixes (`sk_live`, `sk_test`, `AIza`, `AKIA`, `ghp_`, `xox[bp]`), high-entropy
|
|
79
|
+
strings, and variable assignments where a name like `key`, `secret`, `token`, or
|
|
80
|
+
`password` is set to a literal string instead of `process.env.X` or equivalent.
|
|
81
|
+
|
|
82
|
+
**What counts as a finding:**
|
|
83
|
+
- A real credential committed directly in source, e.g.:
|
|
84
|
+
`const stripeKey = "sk_live_51H8x..."` — Critical
|
|
85
|
+
- A secret present only in `.env` but `.env` is not gitignored (see Check 2) —
|
|
86
|
+
Critical, because it WILL leak on the next commit even if it hasn't yet
|
|
87
|
+
- A secret exposed to the client bundle — e.g. a server-only key referenced in
|
|
88
|
+
frontend code, or a Next.js env var missing the required server-only scoping
|
|
89
|
+
(using a secret key where only `NEXT_PUBLIC_`-prefixed vars should appear) —
|
|
90
|
+
Critical, this is directly shippable to every visitor's browser
|
|
91
|
+
|
|
92
|
+
**What is NOT a finding (avoid false positives):**
|
|
93
|
+
- Public/anon/publishable keys that are designed to be exposed client-side
|
|
94
|
+
(Stripe publishable keys starting `pk_`, Supabase anon keys) — these are safe
|
|
95
|
+
by design as long as server-side authorization (RLS, API checks) is correctly
|
|
96
|
+
configured. Note this distinction explicitly in the report so the user isn't
|
|
97
|
+
confused about why one key is fine and another isn't.
|
|
98
|
+
- Example/placeholder values clearly meant as documentation, e.g. `"your-api-key-here"`
|
|
99
|
+
|
|
100
|
+
## Check 2: .gitignore hygiene
|
|
101
|
+
|
|
102
|
+
**Run the script** `scripts/check_gitignore.py` (located relative to this `SKILL.md` file) against the project root. It checks whether `.env`, `.env.local`, and
|
|
103
|
+
similar files exist in the project and whether `.gitignore` actually excludes
|
|
104
|
+
them (not just whether a `.gitignore` file exists — many AI-generated `.gitignore`
|
|
105
|
+
files exist but miss the actual secret file).
|
|
106
|
+
|
|
107
|
+
**What counts as a finding:**
|
|
108
|
+
- `.env` present in the working directory and not listed in `.gitignore` —
|
|
109
|
+
Critical, regardless of current contents
|
|
110
|
+
- `.env` already committed to git history (check with a quick `git log --all
|
|
111
|
+
--full-history -- .env` if git is available) — Critical, and note in the
|
|
112
|
+
fix that removing it from `.gitignore` going forward is not enough; the
|
|
113
|
+
secrets in git history must be rotated, since they're recoverable even after
|
|
114
|
+
deletion
|
|
115
|
+
|
|
116
|
+
## Check 3: Missing or fake authentication
|
|
117
|
+
|
|
118
|
+
**Run the script** `scripts/check_auth_patterns.py` (located relative to this `SKILL.md` file) against the project root as a first pass. Then search auth-related code (login handlers, middleware, route guards, session
|
|
119
|
+
checks) for these patterns:
|
|
120
|
+
|
|
121
|
+
**What counts as a finding:**
|
|
122
|
+
- A function that always returns true/success regardless of input, e.g.:
|
|
123
|
+
```js
|
|
124
|
+
function isAuthenticated(req) {
|
|
125
|
+
return true; // TODO: implement real auth
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
Critical — this is a placeholder someone forgot to replace.
|
|
129
|
+
- Naming that signals a stub: `mockAuth`, `fakeLogin`, `tempAuth`, `bypassAuth`,
|
|
130
|
+
`skipAuth`, `devAuth` still present in a codebase with no clear dev-only guard
|
|
131
|
+
around it. If it IS properly guarded (e.g. `if (process.env.NODE_ENV ===
|
|
132
|
+
'development')`), verify the guard is airtight and note it as Should Fix
|
|
133
|
+
rather than Critical, since misconfigured environment variables in production
|
|
134
|
+
are a common way these leak through anyway.
|
|
135
|
+
- A protected route or API endpoint with no auth check at all — compare route
|
|
136
|
+
definitions against which ones return or modify user-specific data.
|
|
137
|
+
- Client-side-only auth checks — e.g. hiding a button in the UI if not logged in,
|
|
138
|
+
but the underlying API endpoint doesn't independently verify the session.
|
|
139
|
+
This is Critical: anyone can call the API directly, bypassing the UI entirely.
|
|
140
|
+
- Auth checks present but commented out, with a bypass left active nearby.
|
|
141
|
+
|
|
142
|
+
**Judgment note:** this check benefits most from your own reasoning rather than
|
|
143
|
+
pure pattern matching, since real projects name things inconsistently. If
|
|
144
|
+
something looks like it might be a stub but you're not certain, read the
|
|
145
|
+
function body fully before deciding — don't flag based on the name alone, and
|
|
146
|
+
don't clear something based on the name alone either.
|
|
147
|
+
|
|
148
|
+
## Check 4: Database and backend-as-a-service misconfiguration
|
|
149
|
+
|
|
150
|
+
**Run the script** `scripts/check_db_config.py` (located relative to this `SKILL.md` file) against the project root as a first pass.
|
|
151
|
+
|
|
152
|
+
If the project uses **Supabase**: check for row-level security (RLS) policies on
|
|
153
|
+
every table that holds user-specific data. A table with RLS disabled, or enabled
|
|
154
|
+
with a policy like `USING (true)` for `SELECT`/`UPDATE`/`DELETE`, means any
|
|
155
|
+
authenticated (or even anonymous) user can read or modify every row.
|
|
156
|
+
```sql
|
|
157
|
+
-- Finding example: this policy applies to ALL rows for ALL users
|
|
158
|
+
CREATE POLICY "allow all" ON orders FOR SELECT USING (true);
|
|
159
|
+
|
|
160
|
+
-- Correct pattern to recommend as the fix:
|
|
161
|
+
CREATE POLICY "users see own orders" ON orders
|
|
162
|
+
FOR SELECT USING (auth.uid() = user_id);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
If the project uses **Firebase**: check `firestore.rules` or `storage.rules` for
|
|
166
|
+
default-allow states, e.g. `allow read, write: if true;` on collections holding
|
|
167
|
+
user data, or rules left at the Firebase default test-mode state (which expires
|
|
168
|
+
but is often copy-pasted into production-like configs).
|
|
169
|
+
|
|
170
|
+
**Storage buckets**: check whether file storage (Supabase Storage, Firebase
|
|
171
|
+
Storage, S3) is set to public when it holds user-uploaded content that should be
|
|
172
|
+
private (profile documents, private images, receipts).
|
|
173
|
+
|
|
174
|
+
## Check 5: Broken object-level authorization (IDOR)
|
|
175
|
+
|
|
176
|
+
This is the single most common serious flaw in vibe-coded apps and the one most
|
|
177
|
+
worth spending real time on. The pattern: an endpoint takes an ID from the
|
|
178
|
+
request and fetches/modifies a resource using that ID, without checking the
|
|
179
|
+
resource actually belongs to the requesting user.
|
|
180
|
+
|
|
181
|
+
```js
|
|
182
|
+
// Finding example — Critical
|
|
183
|
+
app.get('/api/orders/:id', requireAuth, async (req, res) => {
|
|
184
|
+
const order = await db.query('SELECT * FROM orders WHERE id = $1', [req.params.id]);
|
|
185
|
+
res.json(order); // any logged-in user can read ANY order by guessing/incrementing the id
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// Correct pattern to recommend as the fix
|
|
189
|
+
app.get('/api/orders/:id', requireAuth, async (req, res) => {
|
|
190
|
+
const order = await db.query(
|
|
191
|
+
'SELECT * FROM orders WHERE id = $1 AND user_id = $2',
|
|
192
|
+
[req.params.id, req.user.id]
|
|
193
|
+
);
|
|
194
|
+
if (!order) return res.status(404).json({ error: 'Not found' });
|
|
195
|
+
res.json(order);
|
|
196
|
+
});
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
Check every route that takes an ID as a URL param, query string, or body field
|
|
200
|
+
and touches a database record. This applies to GET (data exposure), PUT/PATCH
|
|
201
|
+
(unauthorized modification), and DELETE (unauthorized deletion) — all three are
|
|
202
|
+
common and DELETE is often the most damaging.
|
|
203
|
+
|
|
204
|
+
## Check 6: Unvalidated inputs
|
|
205
|
+
|
|
206
|
+
Spot-check forms and API endpoints for:
|
|
207
|
+
- No type or length validation on inputs before they're stored or used
|
|
208
|
+
- User input passed into a database query via string concatenation rather than
|
|
209
|
+
parameterized queries/an ORM (SQL injection risk)
|
|
210
|
+
- User input rendered into HTML without escaping (XSS risk), especially in
|
|
211
|
+
frameworks that don't auto-escape by default
|
|
212
|
+
- File uploads with no restriction on file type or size
|
|
213
|
+
|
|
214
|
+
This check doesn't need to be exhaustive — flag the clearest, highest-impact
|
|
215
|
+
examples rather than every single form field, and note in the summary that a
|
|
216
|
+
full input-validation review is a good idea if the codebase is large.
|
|
217
|
+
|
|
218
|
+
## Check 7: Client-side payment or pricing logic
|
|
219
|
+
|
|
220
|
+
Search checkout/payment flows for the price, discount, or total being read from
|
|
221
|
+
data the client controls (a hidden form field, a request body value, a query
|
|
222
|
+
param) rather than looked up server-side from a trusted source.
|
|
223
|
+
|
|
224
|
+
```js
|
|
225
|
+
// Finding example — Critical
|
|
226
|
+
app.post('/api/checkout', async (req, res) => {
|
|
227
|
+
const { productId, price } = req.body; // price is trusted from the client!
|
|
228
|
+
await chargeCard(req.user.paymentMethod, price);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Correct pattern to recommend as the fix
|
|
232
|
+
app.post('/api/checkout', async (req, res) => {
|
|
233
|
+
const { productId } = req.body;
|
|
234
|
+
const product = await db.query('SELECT price FROM products WHERE id = $1', [productId]);
|
|
235
|
+
await chargeCard(req.user.paymentMethod, product.price); // price comes from the server
|
|
236
|
+
});
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
---
|
|
240
|
+
|
|
241
|
+
## Severity rubric
|
|
242
|
+
|
|
243
|
+
- **Critical** — exploitable right now by any user or visitor, exposes real user
|
|
244
|
+
data, or allows bypassing authentication or payment. Ship-blocking.
|
|
245
|
+
- **Should Fix** — a genuine weakness that requires more specific conditions to
|
|
246
|
+
exploit (e.g. requires knowing another user's exact ID, or only affects an
|
|
247
|
+
admin-only route with a smaller blast radius), or a control that exists but is
|
|
248
|
+
incomplete/inconsistent.
|
|
249
|
+
- **Worth Reviewing** — best-practice gap with low immediate exploitability
|
|
250
|
+
given the current app, but worth fixing before the app scales or handles more
|
|
251
|
+
sensitive data.
|
|
252
|
+
|
|
253
|
+
When in doubt between two levels, consider: could a stranger with no special
|
|
254
|
+
access do real harm to a real user right now? If yes, Critical.
|
|
255
|
+
|
|
256
|
+
## Report format
|
|
257
|
+
|
|
258
|
+
For every finding, use exactly this structure:
|
|
259
|
+
|
|
260
|
+
- Include a short `Confidence:` line when the evidence is not fully conclusive.
|
|
261
|
+
- Include a `Generated fix:` line when a direct code snippet or precise implementation step would help the agent patch the issue immediately.
|
|
262
|
+
|
|
263
|
+
**[SEVERITY] Short title**
|
|
264
|
+
- **What's wrong:** plain-English description, no jargon. If a technical term is
|
|
265
|
+
unavoidable (e.g. "IDOR"), define it in one clause the first time it's used.
|
|
266
|
+
- **Why it matters:** a concrete, real-world consequence a non-technical person
|
|
267
|
+
would understand — not "this violates the principle of least privilege" but
|
|
268
|
+
"a stranger could see another user's home address and order history just by
|
|
269
|
+
changing a number in the browser's address bar."
|
|
270
|
+
- **The fix:** a specific code snippet or precise instruction, not a vague
|
|
271
|
+
suggestion like "add proper validation."
|
|
272
|
+
- **File(s):** exact path and line number(s).
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
## After the checks
|
|
276
|
+
|
|
277
|
+
Once the checks are complete, create a security audit/report. **The report must be written in clear, plain English, completely free of dense security jargon, so that it is easily understandable by non-technical stakeholders (such as founders, clients, or project managers).** The report must include:
|
|
278
|
+
- what was found
|
|
279
|
+
- what is fixed already
|
|
280
|
+
- how each fix was applied
|
|
281
|
+
- what still needs attention
|
|
282
|
+
- the final readiness verdict
|
|
283
|
+
|
|
284
|
+
If no issues are found, still produce a short report that says no blocking issues were found in this pass, which categories were checked, and what the remaining scope limits are.
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
## Example report style
|
|
288
|
+
|
|
289
|
+
Use clear, direct wording like:
|
|
290
|
+
|
|
291
|
+
**[Critical] Missing object-level authorization**
|
|
292
|
+
- **What's wrong:** Any logged-in user can read another user's order by changing the order ID.
|
|
293
|
+
- **Why it matters:** A stranger could see someone else's orders and personal details.
|
|
294
|
+
- **The fix:** Add a user ownership check in the query and return 404 when the record does not belong to the current user.
|
|
295
|
+
- **File(s):** `src/routes/orders.ts:42-58`
|
|
296
|
+
|
|
297
|
+
## Final summary
|
|
298
|
+
|
|
299
|
+
End every audit with, in this order:
|
|
300
|
+
1. Stack and data-sensitivity context noted at the start (one line)
|
|
301
|
+
2. Total findings by severity, e.g. "2 Critical, 3 Should Fix, 1 Worth Reviewing"
|
|
302
|
+
3. A one-line verdict: "Not ready to ship — fix the Critical items first" or "No
|
|
303
|
+
blocking issues found in this pass — review the Should Fix items when you can"
|
|
304
|
+
4. The scope reminder: "This covers common patterns seen in AI-generated code —
|
|
305
|
+
exposed secrets, auth stubs, database misconfiguration, IDOR, input validation,
|
|
306
|
+
and client-side payment logic. It is not a full penetration test. If this app
|
|
307
|
+
handles payment, health, or other regulated data, get a professional security
|
|
308
|
+
review before launch regardless of these results."
|
|
309
|
+
|
|
310
|
+
## Notes for the agent
|
|
311
|
+
|
|
312
|
+
- Always produce an audit/report after the scan; do not stop at raw findings.
|
|
313
|
+
- Write the entire report in clear, accessible language. Frame findings around concrete, real-world user consequences rather than abstract technical concepts. A non-technical stakeholder must be able to read the report and immediately understand the real-world danger.
|
|
314
|
+
- The audit should clearly separate what was found, what was fixed, how it was fixed, and what remains.
|
|
315
|
+
- Always run the available scripts (located in the `scripts/` directory relative to this `SKILL.md` file) before relying on reasoning alone for Checks 1-4 — they exist so those specific checks are reliable and repeatable rather than dependent on re-deriving the logic every time.
|
|
316
|
+
- If a script is missing, fails to run, or the language/stack isn't supported by
|
|
317
|
+
it, say so explicitly in the report ("automated secret scan could not run;
|
|
318
|
+
manually reviewed instead") rather than silently skipping the check.
|
|
319
|
+
- Do not invent findings to seem thorough. If a check finds nothing, report "No
|
|
320
|
+
issues found" for that category explicitly — silence looks like the check
|
|
321
|
+
wasn't performed at all.
|
|
322
|
+
- Do not flag the same underlying issue multiple times under different check
|
|
323
|
+
categories — pick the most relevant category and reference it once.
|
|
324
|
+
- If the codebase is large, prioritize routes and files that touch
|
|
325
|
+
authentication, payments, and any endpoint returning data tied to a specific
|
|
326
|
+
user ID — these are where real damage concentrates.
|
package/install.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const readline = require('readline');
|
|
7
|
+
|
|
8
|
+
// Source paths in the npm package
|
|
9
|
+
const srcSkill = path.join(__dirname, 'SKILL.md');
|
|
10
|
+
const srcScripts = path.join(__dirname, 'scripts');
|
|
11
|
+
const srcReferences = path.join(__dirname, 'references');
|
|
12
|
+
|
|
13
|
+
// Destination selection
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const hasLocalClaude = fs.existsSync(path.join(cwd, '.claude'));
|
|
16
|
+
|
|
17
|
+
const localDest = path.join(cwd, '.claude', 'skills', 'checkmyvibe');
|
|
18
|
+
const globalDest = path.join(os.homedir(), '.claude', 'skills', 'checkmyvibe');
|
|
19
|
+
|
|
20
|
+
function copyRecursiveSync(src, dest) {
|
|
21
|
+
if (fs.statSync(src).isDirectory()) {
|
|
22
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
23
|
+
fs.readdirSync(src).forEach((child) => {
|
|
24
|
+
copyRecursiveSync(path.join(src, child), path.join(dest, child));
|
|
25
|
+
});
|
|
26
|
+
} else {
|
|
27
|
+
fs.copyFileSync(src, dest);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function performInstall(destPath) {
|
|
32
|
+
try {
|
|
33
|
+
console.log(`\nInstalling checkmyvibe to: ${destPath}...`);
|
|
34
|
+
|
|
35
|
+
// Clear destination if it already exists (overwrite mode)
|
|
36
|
+
if (fs.existsSync(destPath)) {
|
|
37
|
+
if (fs.rmSync) {
|
|
38
|
+
fs.rmSync(destPath, { recursive: true, force: true });
|
|
39
|
+
} else {
|
|
40
|
+
fs.rmdirSync(destPath, { recursive: true });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
fs.mkdirSync(destPath, { recursive: true });
|
|
44
|
+
|
|
45
|
+
// Copy SKILL.md
|
|
46
|
+
fs.copyFileSync(srcSkill, path.join(destPath, 'SKILL.md'));
|
|
47
|
+
|
|
48
|
+
// Copy folders
|
|
49
|
+
copyRecursiveSync(srcScripts, path.join(destPath, 'scripts'));
|
|
50
|
+
copyRecursiveSync(srcReferences, path.join(destPath, 'references'));
|
|
51
|
+
|
|
52
|
+
console.log('\n=========================================');
|
|
53
|
+
console.log('🎉 checkmyvibe Agent Skill installed!');
|
|
54
|
+
console.log('=========================================');
|
|
55
|
+
console.log('\nHow to run a security audit:');
|
|
56
|
+
console.log('1. Open your terminal in the target repository.');
|
|
57
|
+
console.log('2. Ask your coding agent: "run a security audit"');
|
|
58
|
+
console.log('3. The agent will execute checkmyvibe\'s checks and output a prioritized report!');
|
|
59
|
+
console.log('=========================================\n');
|
|
60
|
+
} catch (err) {
|
|
61
|
+
console.error('Error installing skill:', err.message);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (hasLocalClaude) {
|
|
67
|
+
// If .claude folder exists in the project root, install at project level directly
|
|
68
|
+
performInstall(localDest);
|
|
69
|
+
} else {
|
|
70
|
+
// Prompt user for local vs global/personal installation
|
|
71
|
+
const rl = readline.createInterface({
|
|
72
|
+
input: process.stdin,
|
|
73
|
+
output: process.stdout
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
console.log('No local .claude/ directory detected in the current working directory.');
|
|
77
|
+
console.log(`1. Install locally to current directory: ${path.join('.claude', 'skills', 'checkmyvibe')}`);
|
|
78
|
+
console.log(`2. Install globally/personally: ${globalDest.replace(os.homedir(), '~')}`);
|
|
79
|
+
|
|
80
|
+
rl.question('\nSelect installation destination (1 or 2, default 2): ', (answer) => {
|
|
81
|
+
rl.close();
|
|
82
|
+
const selection = answer.trim();
|
|
83
|
+
if (selection === '1') {
|
|
84
|
+
performInstall(localDest);
|
|
85
|
+
} else {
|
|
86
|
+
performInstall(globalDest);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
3
|
+
"name": "checkmyvibe",
|
|
4
|
+
"version": "1.0.0",
|
|
5
|
+
"description": "A structured security-audit workflow Agent Skill for AI coding assistants to catch common vibe-coded vulnerabilities.",
|
|
6
|
+
"main": "install.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"checkmyvibe": "install.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"SKILL.md",
|
|
12
|
+
"scripts/",
|
|
13
|
+
"references/",
|
|
14
|
+
"install.js",
|
|
15
|
+
"README.md",
|
|
16
|
+
"LICENSE"
|
|
17
|
+
],
|
|
18
|
+
"engines": {
|
|
19
|
+
"node": ">=14.0.0"
|
|
20
|
+
},
|
|
21
|
+
"keywords": [
|
|
22
|
+
"ai-agent",
|
|
23
|
+
"security-audit",
|
|
24
|
+
"claude-code",
|
|
25
|
+
"cursor",
|
|
26
|
+
"vibe-coding",
|
|
27
|
+
"agent-skill"
|
|
28
|
+
],
|
|
29
|
+
"author": "Sarthak",
|
|
30
|
+
"license": "Apache-2.0"
|
|
31
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# Vibe Vulnerability Patterns
|
|
2
|
+
|
|
3
|
+
This document serves as a reference for common vulnerability patterns identified in AI-generated ("vibe-coded") applications. AI coding assistants frequently introduce these security flaws because they prioritize speed, feature completion, and standalone component demonstration over robust security configurations.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## 1. Exposed Secrets
|
|
8
|
+
|
|
9
|
+
### Overview
|
|
10
|
+
AI assistants often scaffold code with hardcoded API keys, JWT secrets, database connection strings, or third-party client keys directly inside configuration files or source code. Additionally, local environment files (`.env`) are frequently created without updating the project's `.gitignore` to prevent them from being committed to version control.
|
|
11
|
+
|
|
12
|
+
### Why it happens in AI-generated code
|
|
13
|
+
[Placeholder: The user will fill in real-world examples and analysis of why this happens in AI-generated code.]
|
|
14
|
+
|
|
15
|
+
### The Correct Fix
|
|
16
|
+
[Placeholder: The user will fill in the correct fix and mitigation strategies.]
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 2. Missing/Fake Authentication (Fake/Stubbed Auth)
|
|
21
|
+
|
|
22
|
+
### Overview
|
|
23
|
+
When generating authentication flows, AI tools often implement mock authentication functions, fake login buttons, or bypass logic (e.g. returning `true` or dummy user objects unconditionally) to make testing quick. Developers may unknowingly deploy this placeholder logic directly to production, leaving endpoints unprotected.
|
|
24
|
+
|
|
25
|
+
### Why it happens in AI-generated code
|
|
26
|
+
[Placeholder: The user will fill in real-world examples and analysis of why this happens in AI-generated code.]
|
|
27
|
+
|
|
28
|
+
### The Correct Fix
|
|
29
|
+
[Placeholder: The user will fill in the correct fix and mitigation strategies.]
|
|
30
|
+
|
|
31
|
+
---
|
|
32
|
+
|
|
33
|
+
## 3. Database Misconfiguration (Permissive Database Rules / RLS)
|
|
34
|
+
|
|
35
|
+
### Overview
|
|
36
|
+
Services like Supabase or Firebase Firestore rely on Row-Level Security (RLS) or security rules files to protect data access. AI assistants frequently scaffold projects with default allow-all rules (e.g. `allow read, write: if true;` or `CREATE POLICY ... TO public USING (true);`) or create tables in Supabase without enabling RLS, enabling any client with an anonymous key to query or modify all records.
|
|
37
|
+
|
|
38
|
+
### Why it happens in AI-generated code
|
|
39
|
+
[Placeholder: The user will fill in real-world examples and analysis of why this happens in AI-generated code.]
|
|
40
|
+
|
|
41
|
+
### The Correct Fix
|
|
42
|
+
[Placeholder: The user will fill in the correct fix and mitigation strategies.]
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 4. Broken Object-Level Authorization (BOLA / IDOR)
|
|
47
|
+
|
|
48
|
+
### Overview
|
|
49
|
+
Broken Object-Level Authorization occurs when an application exposes a resource endpoint (e.g. `/api/orders/:id` or fetching data by a user-supplied ID parameter) and retrieves or updates the resource without verifying that the authenticated user owns or has permission to access that specific resource.
|
|
50
|
+
|
|
51
|
+
### Why it happens in AI-generated code
|
|
52
|
+
[Placeholder: The user will fill in real-world examples and analysis of why this happens in AI-generated code.]
|
|
53
|
+
|
|
54
|
+
### The Correct Fix
|
|
55
|
+
[Placeholder: The user will fill in the correct fix and mitigation strategies.]
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 5. Unvalidated Inputs
|
|
60
|
+
|
|
61
|
+
### Overview
|
|
62
|
+
AI code often lacks backend input validation, trusting that client-side forms are sufficient. This opens the door to SQL injection, Cross-Site Scripting (XSS), server-side crashes due to unexpected data formats, or logic bypasses.
|
|
63
|
+
|
|
64
|
+
### Why it happens in AI-generated code
|
|
65
|
+
[Placeholder: The user will fill in real-world examples and analysis of why this happens in AI-generated code.]
|
|
66
|
+
|
|
67
|
+
### The Correct Fix
|
|
68
|
+
[Placeholder: The user will fill in the correct fix and mitigation strategies.]
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## 6. Client-Side Payment Logic
|
|
73
|
+
|
|
74
|
+
### Overview
|
|
75
|
+
AI tools often write payment flows where pricing calculations, transaction amounts, subscription statuses, or checkout processes are determined or validated entirely on the client side. Attackers can easily intercept and modify request bodies (e.g. setting a premium product price to `$0.00`) before sending it to a payment gateway API.
|
|
76
|
+
|
|
77
|
+
### Why it happens in AI-generated code
|
|
78
|
+
[Placeholder: The user will fill in real-world examples and analysis of why this happens in AI-generated code.]
|
|
79
|
+
|
|
80
|
+
### The Correct Fix
|
|
81
|
+
[Placeholder: The user will fill in the correct fix and mitigation strategies.]
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
SKIP_DIRS = {
|
|
7
|
+
'node_modules', '.git', 'build', 'dist', '.next', '.svelte-kit',
|
|
8
|
+
'__pycache__', 'venv', '.venv', 'env', 'coverage', 'out'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
SKIP_EXTS = {
|
|
12
|
+
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.pdf', '.zip', '.tar',
|
|
13
|
+
'.gz', '.mp3', '.mp4', '.mov', '.db', '.sqlite', '.exe', '.dll',
|
|
14
|
+
'.so', '.dylib', '.woff', '.woff2', '.ttf', '.eot', '.lock',
|
|
15
|
+
'.package-lock.json', '.pnpm-lock.yaml', '.yarn.lock', '.map',
|
|
16
|
+
'.svg', '.css', '.scss', '.less', '.html', '.xml'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
# 1. Naming patterns for fake/mock/bypass authentication
|
|
20
|
+
AUTH_KEYWORDS = [
|
|
21
|
+
r'mockAuth', r'fakeLogin', r'tempAuth', r'bypassAuth',
|
|
22
|
+
r'mockLogin', r'fakeAuth', r'bypassLogin', r'dummyAuth',
|
|
23
|
+
r'bypassUser', r'mockUser', r'fakeUser'
|
|
24
|
+
]
|
|
25
|
+
KEYWORD_REGEX = re.compile(r'(?i)\b(' + '|'.join(AUTH_KEYWORDS) + r')\b')
|
|
26
|
+
|
|
27
|
+
# 2. Stubbed functions or parameters returning true (always passing)
|
|
28
|
+
STUB_PATTERNS = [
|
|
29
|
+
# JS arrow function returning true: e.g. checkAuth = () => true
|
|
30
|
+
(re.compile(r'(?i)\b\w*(?:auth|login|signin|signup|admin|role|permission|credential)\w*\b\s*=\s*(?:\([^)]*\)|[a-zA-Z_]\w*)\s*=>\s*true\b(?!\s*==)'),
|
|
31
|
+
"Arrow function returning true directly"),
|
|
32
|
+
|
|
33
|
+
# JS standard function returning true: function checkAuth(...) { return true; }
|
|
34
|
+
(re.compile(r'(?i)\bfunction\s+\w*(?:auth|login|signin|signup|admin|role|permission|credential)\w*\b\s*\([^)]*\)\s*\{\s*return\s+true;?\s*\}'),
|
|
35
|
+
"Function returning true directly"),
|
|
36
|
+
|
|
37
|
+
# Python function returning True: def is_auth(...): return True
|
|
38
|
+
(re.compile(r'(?i)\bdef\s+\w*(?:auth|login|signin|signup|admin|role|permission|credential)\w*\b\s*\([^)]*\)\s*:\s*return\s+True\b'),
|
|
39
|
+
"Python function returning True directly"),
|
|
40
|
+
|
|
41
|
+
# Generic bypass function or property: e.g. "authBypass: true", "skipAuth: true"
|
|
42
|
+
(re.compile(r'(?i)\b(?:authBypass|skipAuth|disableAuth|bypassAuthentication|devAuth)\b\s*:\s*true\b'),
|
|
43
|
+
"Authentication bypass flag set to true")
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
def is_binary(file_path):
|
|
47
|
+
try:
|
|
48
|
+
with open(file_path, 'rb') as f:
|
|
49
|
+
chunk = f.read(1024)
|
|
50
|
+
return b'\x00' in chunk
|
|
51
|
+
except Exception:
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
def scan_file(file_path):
|
|
55
|
+
findings = []
|
|
56
|
+
if is_binary(file_path):
|
|
57
|
+
return findings
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
61
|
+
content = f.read()
|
|
62
|
+
|
|
63
|
+
lines = content.splitlines()
|
|
64
|
+
|
|
65
|
+
# 1. Search for keyword matches line by line
|
|
66
|
+
for line_num, line in enumerate(lines, 1):
|
|
67
|
+
match = KEYWORD_REGEX.search(line)
|
|
68
|
+
if match:
|
|
69
|
+
findings.append((line_num, f"Mock/Bypass auth keyword '{match.group(1)}' found", line.strip()))
|
|
70
|
+
|
|
71
|
+
# 2. Search for stub patterns in the file content
|
|
72
|
+
for pattern, desc in STUB_PATTERNS:
|
|
73
|
+
for match in pattern.finditer(content):
|
|
74
|
+
start_pos = match.start()
|
|
75
|
+
# Find line number of this match
|
|
76
|
+
line_num = content.count('\n', 0, start_pos) + 1
|
|
77
|
+
matched_text = match.group(0).replace('\n', ' ').strip()
|
|
78
|
+
# Avoid adding multiple duplicate findings for the same line
|
|
79
|
+
if not any(f[0] == line_num for f in findings):
|
|
80
|
+
findings.append((line_num, f"Stubbed Auth: {desc}", matched_text))
|
|
81
|
+
|
|
82
|
+
except Exception:
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
findings.sort(key=lambda x: x[0])
|
|
86
|
+
return findings
|
|
87
|
+
|
|
88
|
+
def main():
|
|
89
|
+
target_dir = sys.argv[1] if len(sys.argv) > 1 else '.'
|
|
90
|
+
target_dir = os.path.abspath(target_dir)
|
|
91
|
+
|
|
92
|
+
if not os.path.exists(target_dir):
|
|
93
|
+
print(f"Error: Path '{target_dir}' does not exist.")
|
|
94
|
+
sys.exit(1)
|
|
95
|
+
|
|
96
|
+
for root, dirs, files in os.walk(target_dir):
|
|
97
|
+
dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith('.')]
|
|
98
|
+
|
|
99
|
+
for file in files:
|
|
100
|
+
ext = os.path.splitext(file)[1].lower()
|
|
101
|
+
if ext in SKIP_EXTS or file.startswith('.'):
|
|
102
|
+
continue
|
|
103
|
+
|
|
104
|
+
file_path = os.path.join(root, file)
|
|
105
|
+
rel_path = os.path.relpath(file_path, target_dir)
|
|
106
|
+
|
|
107
|
+
findings = scan_file(file_path)
|
|
108
|
+
for line_num, desc, context in findings:
|
|
109
|
+
print(f"{rel_path}:{line_num}: {desc} -> `{context}`")
|
|
110
|
+
|
|
111
|
+
if __name__ == '__main__':
|
|
112
|
+
main()
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import re
|
|
5
|
+
|
|
6
|
+
SKIP_DIRS = {'node_modules', '.git', 'dist', 'build'}
|
|
7
|
+
|
|
8
|
+
# Permissive rules patterns for Firestore and Firebase Storage
|
|
9
|
+
FIREBASE_PERMISSIVE = re.compile(r'allow\s+[\w\s,]+:\s*if\s+true\s*;')
|
|
10
|
+
|
|
11
|
+
# Permissive Realtime Database rules
|
|
12
|
+
RTDB_PERMISSIVE = re.compile(r'"\.(read|write)"\s*:\s*"true"')
|
|
13
|
+
|
|
14
|
+
# Permissive policies in Supabase / SQL schema definition
|
|
15
|
+
SUPABASE_PERMISSIVE_POLICY = re.compile(r'(?i)create\s+policy\s+\w+\s+on\s+\w+\s+(?:for\s+\w+\s+)?to\s+(?:public|anon)\s+using\s*\(\s*true\s*\)')
|
|
16
|
+
SUPABASE_PERMISSIVE_POLICY_SIMPLE = re.compile(r'(?i)using\s*\(\s*true\s*\)|with\s+check\s*\(\s*true\s*\)')
|
|
17
|
+
|
|
18
|
+
def scan_firebase_rules(file_path):
|
|
19
|
+
"""Scan Firestore/Storage rules for allow-all statements."""
|
|
20
|
+
findings = []
|
|
21
|
+
try:
|
|
22
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
23
|
+
for line_num, line in enumerate(f, 1):
|
|
24
|
+
clean_line = line.strip()
|
|
25
|
+
if FIREBASE_PERMISSIVE.search(clean_line):
|
|
26
|
+
findings.append((line_num, f"Permissive allow-all rule found: `{clean_line}`"))
|
|
27
|
+
except Exception:
|
|
28
|
+
pass
|
|
29
|
+
return findings
|
|
30
|
+
|
|
31
|
+
def scan_rtdb_rules(file_path):
|
|
32
|
+
"""Scan Firebase Realtime Database rules for true reads/writes."""
|
|
33
|
+
findings = []
|
|
34
|
+
try:
|
|
35
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
36
|
+
for line_num, line in enumerate(f, 1):
|
|
37
|
+
clean_line = line.strip()
|
|
38
|
+
if RTDB_PERMISSIVE.search(clean_line):
|
|
39
|
+
findings.append((line_num, f"Permissive RTDB allow-all rule found: `{clean_line}`"))
|
|
40
|
+
except Exception:
|
|
41
|
+
pass
|
|
42
|
+
return findings
|
|
43
|
+
|
|
44
|
+
def scan_sql_rls(file_path):
|
|
45
|
+
"""Scan SQL files to check if RLS is enabled on all tables and check for permissive policies."""
|
|
46
|
+
findings = []
|
|
47
|
+
try:
|
|
48
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
49
|
+
content = f.read()
|
|
50
|
+
|
|
51
|
+
# 1. Strip comments to avoid false matches (e.g. commented-out commands or code annotations)
|
|
52
|
+
# Strip single-line -- comments
|
|
53
|
+
content_no_comments = re.sub(r'--.*$', '', content, flags=re.M)
|
|
54
|
+
# Strip multiline /* */ comments
|
|
55
|
+
content_no_comments = re.sub(r'/\*.*?\*/', '', content_no_comments, flags=re.S)
|
|
56
|
+
|
|
57
|
+
# 2. Find all CREATE TABLE statements (support schemas, quotes, etc.)
|
|
58
|
+
created_tables = re.findall(r'(?i)create\s+table\s+(?:if\s+not\s+exists\s+)?([\w\.]+)', content_no_comments)
|
|
59
|
+
# Find all ENABLE ROW LEVEL SECURITY statements
|
|
60
|
+
enabled_rls = re.findall(r'(?i)alter\s+table\s+(?:if\s+exists\s+)?([\w\.]+)\s+enable\s+row\s+level\s+security', content_no_comments)
|
|
61
|
+
|
|
62
|
+
# Clean table names (strip quotes, normalize schemas)
|
|
63
|
+
def clean_table_name(t):
|
|
64
|
+
t = t.replace('"', '').replace("'", "")
|
|
65
|
+
if '.' in t:
|
|
66
|
+
t = t.split('.')[-1]
|
|
67
|
+
return t.lower()
|
|
68
|
+
|
|
69
|
+
created_clean = [clean_table_name(t) for t in created_tables]
|
|
70
|
+
enabled_clean = [clean_table_name(t) for t in enabled_rls]
|
|
71
|
+
|
|
72
|
+
for idx, table in enumerate(created_tables):
|
|
73
|
+
clean_t = clean_table_name(table)
|
|
74
|
+
if clean_t not in enabled_clean:
|
|
75
|
+
# Find line number in original file
|
|
76
|
+
escaped_t = re.escape(table)
|
|
77
|
+
match = re.search(r'(?i)create\s+table\s+(?:if\s+not\s+exists\s+)?' + escaped_t, content)
|
|
78
|
+
line_num = content.count('\n', 0, match.start()) + 1 if match else 1
|
|
79
|
+
findings.append((line_num, f"Table '{table}' is created but Row Level Security (RLS) is not enabled"))
|
|
80
|
+
|
|
81
|
+
# 3. Find permissive policies:
|
|
82
|
+
# Search for CREATE POLICY ... USING (true) or WITH CHECK (true)
|
|
83
|
+
# Handles multiline policy definitions and quoted policy names
|
|
84
|
+
policy_pattern = re.compile(
|
|
85
|
+
r'(?i)create\s+policy\s+("[^"]+"|[a-zA-Z_]\w*)\s+on\s+([\w\.]+)\s+[^;]+?(?:using|with\s+check)\s*\(\s*true\s*\)',
|
|
86
|
+
re.DOTALL
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
for match in policy_pattern.finditer(content_no_comments):
|
|
90
|
+
policy_name = match.group(1)
|
|
91
|
+
table_name = match.group(2)
|
|
92
|
+
# Find the character position of this policy match in the original content
|
|
93
|
+
orig_match = re.search(r'(?i)create\s+policy\s+' + re.escape(policy_name), content)
|
|
94
|
+
line_num = content.count('\n', 0, orig_match.start()) + 1 if orig_match else 1
|
|
95
|
+
findings.append((line_num, f"Permissive policy {policy_name} on table {table_name} allows public read/write via USING(true) / CHECK(true)"))
|
|
96
|
+
|
|
97
|
+
except Exception:
|
|
98
|
+
pass
|
|
99
|
+
return findings
|
|
100
|
+
|
|
101
|
+
def main():
|
|
102
|
+
target_dir = sys.argv[1] if len(sys.argv) > 1 else '.'
|
|
103
|
+
target_dir = os.path.abspath(target_dir)
|
|
104
|
+
|
|
105
|
+
findings_by_file = {}
|
|
106
|
+
|
|
107
|
+
for root, dirs, files in os.walk(target_dir):
|
|
108
|
+
dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith('.')]
|
|
109
|
+
|
|
110
|
+
for file in files:
|
|
111
|
+
file_path = os.path.join(root, file)
|
|
112
|
+
rel_path = os.path.relpath(file_path, target_dir)
|
|
113
|
+
|
|
114
|
+
# Check for Firebase Firestore/Storage rules
|
|
115
|
+
if file in ['firestore.rules', 'storage.rules']:
|
|
116
|
+
file_findings = scan_firebase_rules(file_path)
|
|
117
|
+
if file_findings:
|
|
118
|
+
findings_by_file[rel_path] = file_findings
|
|
119
|
+
|
|
120
|
+
# Check for Firebase Realtime Database rules
|
|
121
|
+
elif file == 'database.rules.json':
|
|
122
|
+
file_findings = scan_rtdb_rules(file_path)
|
|
123
|
+
if file_findings:
|
|
124
|
+
findings_by_file[rel_path] = file_findings
|
|
125
|
+
|
|
126
|
+
# Check for SQL migrations/schema files
|
|
127
|
+
elif file.endswith('.sql'):
|
|
128
|
+
file_findings = scan_sql_rls(file_path)
|
|
129
|
+
if file_findings:
|
|
130
|
+
findings_by_file[rel_path] = file_findings
|
|
131
|
+
|
|
132
|
+
# Output findings
|
|
133
|
+
if not findings_by_file:
|
|
134
|
+
print("PASS: No database security misconfigurations detected in configuration files.")
|
|
135
|
+
sys.exit(0)
|
|
136
|
+
|
|
137
|
+
print("FAIL: The following database security issues were found:")
|
|
138
|
+
for rel_path, file_findings in findings_by_file.items():
|
|
139
|
+
for line_num, desc in file_findings:
|
|
140
|
+
print(f"{rel_path}:{line_num}: {desc}")
|
|
141
|
+
sys.exit(1)
|
|
142
|
+
|
|
143
|
+
if __name__ == '__main__':
|
|
144
|
+
main()
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import fnmatch
|
|
5
|
+
|
|
6
|
+
# Potential local secret file patterns (glob format)
|
|
7
|
+
SECRET_PATTERNS = [
|
|
8
|
+
'.env',
|
|
9
|
+
'.env.*',
|
|
10
|
+
'*service-account*.json',
|
|
11
|
+
'*serviceAccount*.json',
|
|
12
|
+
'*credentials*.json',
|
|
13
|
+
'*.pem',
|
|
14
|
+
'*.key',
|
|
15
|
+
'*.p12',
|
|
16
|
+
'secrets.json',
|
|
17
|
+
'jwt_key*',
|
|
18
|
+
]
|
|
19
|
+
|
|
20
|
+
# Patterns that are allowed to be public (not secrets)
|
|
21
|
+
PUBLIC_EXCEPTIONS = [
|
|
22
|
+
'*.example',
|
|
23
|
+
'*.template',
|
|
24
|
+
'*.sample',
|
|
25
|
+
'*.dist',
|
|
26
|
+
'*.config.json',
|
|
27
|
+
'firebase.json',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
def is_secret_file(filename):
|
|
31
|
+
"""Check if filename matches secret patterns and doesn't match public exceptions."""
|
|
32
|
+
is_secret = False
|
|
33
|
+
for pat in SECRET_PATTERNS:
|
|
34
|
+
if fnmatch.fnmatch(filename, pat):
|
|
35
|
+
is_secret = True
|
|
36
|
+
break
|
|
37
|
+
|
|
38
|
+
if not is_secret:
|
|
39
|
+
return False
|
|
40
|
+
|
|
41
|
+
for pat in PUBLIC_EXCEPTIONS:
|
|
42
|
+
if fnmatch.fnmatch(filename, pat):
|
|
43
|
+
return False
|
|
44
|
+
|
|
45
|
+
return True
|
|
46
|
+
|
|
47
|
+
def parse_gitignore(gitignore_path):
|
|
48
|
+
"""Read and parse the .gitignore patterns, skipping comments and empty lines."""
|
|
49
|
+
patterns = []
|
|
50
|
+
if not os.path.exists(gitignore_path):
|
|
51
|
+
return patterns
|
|
52
|
+
|
|
53
|
+
with open(gitignore_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
54
|
+
for line in f:
|
|
55
|
+
line = line.strip()
|
|
56
|
+
if not line or line.startswith('#'):
|
|
57
|
+
continue
|
|
58
|
+
# Strip inline comments
|
|
59
|
+
if ' #' in line:
|
|
60
|
+
line = line.split(' #')[0].strip()
|
|
61
|
+
patterns.append(line)
|
|
62
|
+
return patterns
|
|
63
|
+
|
|
64
|
+
def is_ignored(file_rel_path, gitignore_patterns):
|
|
65
|
+
"""Verify if a relative file path matches any .gitignore pattern."""
|
|
66
|
+
ignored = False
|
|
67
|
+
parts = file_rel_path.split(os.sep)
|
|
68
|
+
|
|
69
|
+
# Hardcoded check for system folders that are assumed ignored
|
|
70
|
+
if 'node_modules' in parts or '.git' in parts or 'dist' in parts or 'build' in parts:
|
|
71
|
+
return True
|
|
72
|
+
|
|
73
|
+
for pattern in gitignore_patterns:
|
|
74
|
+
negate = False
|
|
75
|
+
if pattern.startswith('!'):
|
|
76
|
+
negate = True
|
|
77
|
+
pattern = pattern[1:]
|
|
78
|
+
|
|
79
|
+
match = False
|
|
80
|
+
if pattern.startswith('/'):
|
|
81
|
+
clean_pattern = pattern[1:]
|
|
82
|
+
match = fnmatch.fnmatch(file_rel_path, clean_pattern) or fnmatch.fnmatch(file_rel_path, clean_pattern + '/*')
|
|
83
|
+
else:
|
|
84
|
+
match = fnmatch.fnmatch(os.path.basename(file_rel_path), pattern) or \
|
|
85
|
+
fnmatch.fnmatch(file_rel_path, pattern) or \
|
|
86
|
+
fnmatch.fnmatch(file_rel_path, '*/' + pattern) or \
|
|
87
|
+
any(fnmatch.fnmatch(part, pattern) for part in parts)
|
|
88
|
+
|
|
89
|
+
if match:
|
|
90
|
+
ignored = not negate
|
|
91
|
+
|
|
92
|
+
return ignored
|
|
93
|
+
|
|
94
|
+
def main():
|
|
95
|
+
target_dir = sys.argv[1] if len(sys.argv) > 1 else '.'
|
|
96
|
+
target_dir = os.path.abspath(target_dir)
|
|
97
|
+
|
|
98
|
+
gitignore_path = os.path.join(target_dir, '.gitignore')
|
|
99
|
+
has_gitignore = os.path.exists(gitignore_path)
|
|
100
|
+
|
|
101
|
+
gitignore_patterns = parse_gitignore(gitignore_path) if has_gitignore else []
|
|
102
|
+
|
|
103
|
+
secret_files_found = []
|
|
104
|
+
unignored_secrets = []
|
|
105
|
+
|
|
106
|
+
for root, dirs, files in os.walk(target_dir):
|
|
107
|
+
# Exclude directories
|
|
108
|
+
dirs[:] = [d for d in dirs if d not in {'node_modules', '.git', 'dist', 'build', '.next', '.svelte-kit'}]
|
|
109
|
+
|
|
110
|
+
for file in files:
|
|
111
|
+
if is_secret_file(file):
|
|
112
|
+
file_path = os.path.join(root, file)
|
|
113
|
+
rel_path = os.path.relpath(file_path, target_dir)
|
|
114
|
+
|
|
115
|
+
if is_ignored(rel_path, gitignore_patterns):
|
|
116
|
+
secret_files_found.append((rel_path, True))
|
|
117
|
+
else:
|
|
118
|
+
secret_files_found.append((rel_path, False))
|
|
119
|
+
unignored_secrets.append(rel_path)
|
|
120
|
+
|
|
121
|
+
if not secret_files_found:
|
|
122
|
+
print("PASS: No local secret files (like .env) were detected in the project.")
|
|
123
|
+
sys.exit(0)
|
|
124
|
+
|
|
125
|
+
if unignored_secrets:
|
|
126
|
+
print("FAIL: The following local secret files are NOT excluded by .gitignore:")
|
|
127
|
+
for sf in unignored_secrets:
|
|
128
|
+
print(f" - {sf}")
|
|
129
|
+
if not has_gitignore:
|
|
130
|
+
print(" (Note: No .gitignore file was found in the project root)")
|
|
131
|
+
sys.exit(1)
|
|
132
|
+
else:
|
|
133
|
+
print("PASS: All detected local secret files are correctly listed in .gitignore:")
|
|
134
|
+
for sf, ignored in secret_files_found:
|
|
135
|
+
print(f" - {sf} (ignored)")
|
|
136
|
+
sys.exit(0)
|
|
137
|
+
|
|
138
|
+
if __name__ == '__main__':
|
|
139
|
+
main()
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import re
|
|
5
|
+
import math
|
|
6
|
+
|
|
7
|
+
# Directories to skip during scanning
|
|
8
|
+
SKIP_DIRS = {
|
|
9
|
+
'node_modules', '.git', 'build', 'dist', '.next', '.svelte-kit',
|
|
10
|
+
'__pycache__', 'venv', '.venv', 'env', 'coverage', 'out', '.expo',
|
|
11
|
+
'.output', '.nuxt'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
# File extensions to skip (binary/media/lockfiles/assets)
|
|
15
|
+
SKIP_EXTS = {
|
|
16
|
+
'.png', '.jpg', '.jpeg', '.gif', '.ico', '.pdf', '.zip', '.tar',
|
|
17
|
+
'.gz', '.mp3', '.mp4', '.mov', '.db', '.sqlite', '.exe', '.dll',
|
|
18
|
+
'.so', '.dylib', '.woff', '.woff2', '.ttf', '.eot', '.lock',
|
|
19
|
+
'.package-lock.json', '.pnpm-lock.yaml', '.yarn.lock', '.map',
|
|
20
|
+
'.svg', '.css', '.scss', '.less', '.html', '.xml'
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
# Regexes for known API key formats
|
|
24
|
+
API_KEY_PATTERNS = [
|
|
25
|
+
(re.compile(r'AIza[0-9A-Za-z-_]{35}'), 'Google API Key'),
|
|
26
|
+
(re.compile(r'AKIA[0-9A-Z]{16}'), 'AWS Access Key ID'),
|
|
27
|
+
(re.compile(r'ghp_[a-zA-Z0-9]{36,40}'), 'GitHub Personal Access Token'),
|
|
28
|
+
(re.compile(r'sk_(live|test)_[0-9a-zA-Z]{24,96}'), 'Stripe Secret Key'),
|
|
29
|
+
(re.compile(r'xox[bapr]-[0-9]{12}-[0-9]{12}-[0-9]{12}-[a-z0-9]{32}'), 'Slack Token'),
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
# Regex for variable assignments with potential hardcoded secrets
|
|
33
|
+
# Matches: name = "string" or "name": "string", where name contains key, secret, token, password, credential, auth
|
|
34
|
+
ASSIGNMENT_PATTERN = re.compile(
|
|
35
|
+
r'(?i)(?:[\'"`]?)\b(\w*(?:key|secret|token|password|credential|auth)\w*)\b(?:[\'"`]?)\s*(?:=|:)\s*([\'"`])([^\'"`\n\r]{8,256})\2'
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Regex for standalone high-entropy candidate words (length 32 to 128)
|
|
39
|
+
HIGH_ENTROPY_WORD_PATTERN = re.compile(r'\b([a-zA-Z0-9+/=_-]{32,128})\b')
|
|
40
|
+
|
|
41
|
+
def calculate_entropy(s):
|
|
42
|
+
"""Calculate the Shannon entropy of a string."""
|
|
43
|
+
if not s:
|
|
44
|
+
return 0
|
|
45
|
+
entropy = 0
|
|
46
|
+
for x in set(s):
|
|
47
|
+
p_x = s.count(x) / len(s)
|
|
48
|
+
entropy -= p_x * math.log2(p_x)
|
|
49
|
+
return entropy
|
|
50
|
+
|
|
51
|
+
def is_binary(file_path):
|
|
52
|
+
"""Check if a file is binary by looking for null bytes."""
|
|
53
|
+
try:
|
|
54
|
+
with open(file_path, 'rb') as f:
|
|
55
|
+
chunk = f.read(1024)
|
|
56
|
+
return b'\x00' in chunk
|
|
57
|
+
except Exception:
|
|
58
|
+
return True
|
|
59
|
+
|
|
60
|
+
def scan_file(file_path):
|
|
61
|
+
findings = []
|
|
62
|
+
if is_binary(file_path):
|
|
63
|
+
return findings
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
67
|
+
for line_num, line in enumerate(f, 1):
|
|
68
|
+
# 1. Check known API key patterns
|
|
69
|
+
for pattern, name in API_KEY_PATTERNS:
|
|
70
|
+
if pattern.search(line):
|
|
71
|
+
findings.append((line_num, f"Exposed {name}"))
|
|
72
|
+
|
|
73
|
+
# 2. Check variable assignments
|
|
74
|
+
for match in ASSIGNMENT_PATTERN.finditer(line):
|
|
75
|
+
var_name, quote, val = match.groups()
|
|
76
|
+
|
|
77
|
+
# Ignore environment variable references, string interpolation, or placeholders
|
|
78
|
+
if any(x in val for x in ['process.env', 'os.environ', 'os.getenv', 'ENV[', '${', '{{', '}}']):
|
|
79
|
+
continue
|
|
80
|
+
|
|
81
|
+
# Ignore common placeholder value patterns
|
|
82
|
+
val_lower = val.lower()
|
|
83
|
+
placeholders = {
|
|
84
|
+
'true', 'false', 'null', 'undefined', 'placeholder', 'dummy',
|
|
85
|
+
'your_key_here', 'your-key-here', 'your_secret_here', 'your-secret-here',
|
|
86
|
+
'test', 'mock', 'none', 'value', 'secret', 'password', 'key', 'token'
|
|
87
|
+
}
|
|
88
|
+
if val_lower in placeholders or all(c in '-_*' for c in val):
|
|
89
|
+
continue
|
|
90
|
+
|
|
91
|
+
entropy = calculate_entropy(val)
|
|
92
|
+
if len(val) >= 12 and entropy > 3.0:
|
|
93
|
+
findings.append((line_num, f"Potential secret assigned to '{var_name}' (entropy: {entropy:.2f})"))
|
|
94
|
+
elif len(val) >= 16:
|
|
95
|
+
findings.append((line_num, f"Suspicious long literal assigned to '{var_name}'"))
|
|
96
|
+
|
|
97
|
+
# 3. Check for standalone high-entropy words (only if not already matched above)
|
|
98
|
+
# Keep threshold high to reduce noise from base64 assets/hashes
|
|
99
|
+
for match in HIGH_ENTROPY_WORD_PATTERN.finditer(line):
|
|
100
|
+
word = match.group(1)
|
|
101
|
+
# Skip if it is part of a URL, or has common non-secret structures
|
|
102
|
+
if any(x in line for x in ['http://', 'https://', 'src=', 'href=', 'url(']):
|
|
103
|
+
continue
|
|
104
|
+
|
|
105
|
+
# Skip common long words/hashes that are not secrets (e.g. standard CSS classes, hex colors)
|
|
106
|
+
if re.match(r'^[0-9a-fA-F]+$', word) and len(word) < 40:
|
|
107
|
+
# Small hex hashes are common (commit IDs, etc.) - require higher length or entropy
|
|
108
|
+
continue
|
|
109
|
+
|
|
110
|
+
entropy = calculate_entropy(word)
|
|
111
|
+
# True high entropy strings (like keys) typically have entropy > 4.2 for 32+ char length
|
|
112
|
+
if len(word) >= 32 and entropy > 4.3:
|
|
113
|
+
# Make sure it's not already reported in assignments
|
|
114
|
+
if not any(word in f[1] for f in findings):
|
|
115
|
+
findings.append((line_num, f"Standalone high-entropy string found (entropy: {entropy:.2f})"))
|
|
116
|
+
|
|
117
|
+
except Exception:
|
|
118
|
+
pass
|
|
119
|
+
|
|
120
|
+
return findings
|
|
121
|
+
|
|
122
|
+
def main():
|
|
123
|
+
target_dir = sys.argv[1] if len(sys.argv) > 1 else '.'
|
|
124
|
+
target_dir = os.path.abspath(target_dir)
|
|
125
|
+
|
|
126
|
+
if not os.path.exists(target_dir):
|
|
127
|
+
print(f"Error: Path '{target_dir}' does not exist.")
|
|
128
|
+
sys.exit(1)
|
|
129
|
+
|
|
130
|
+
for root, dirs, files in os.walk(target_dir):
|
|
131
|
+
# Exclude directories in-place
|
|
132
|
+
dirs[:] = [d for d in dirs if d not in SKIP_DIRS and not d.startswith('.')]
|
|
133
|
+
|
|
134
|
+
for file in files:
|
|
135
|
+
ext = os.path.splitext(file)[1].lower()
|
|
136
|
+
# Skip hidden files unless they are .env configuration files
|
|
137
|
+
if ext in SKIP_EXTS or (file.startswith('.') and not file.startswith('.env')):
|
|
138
|
+
continue
|
|
139
|
+
|
|
140
|
+
file_path = os.path.join(root, file)
|
|
141
|
+
rel_path = os.path.relpath(file_path, target_dir)
|
|
142
|
+
|
|
143
|
+
findings = scan_file(file_path)
|
|
144
|
+
for line_num, desc in findings:
|
|
145
|
+
print(f"{rel_path}:{line_num}: {desc}")
|
|
146
|
+
|
|
147
|
+
if __name__ == '__main__':
|
|
148
|
+
main()
|