secure-push-check 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/README.md ADDED
@@ -0,0 +1,218 @@
1
+ # secure-push-check
2
+
3
+ `secure-push-check` is a production-ready Node.js CLI that scans a local Git repository for common security risks before you push to GitHub.
4
+
5
+ It detects:
6
+ - Hardcoded secrets in text files (regex-based, configurable)
7
+ - Sensitive files accidentally tracked by Git
8
+ - Missing sensitive patterns in `.gitignore`
9
+ - Dependency vulnerabilities from `npm audit --json`
10
+ - Hardcoded credentials in JS/TS via Babel AST parsing
11
+
12
+ ## Features
13
+
14
+ - Fully async ESM implementation (`Node.js >= 18`)
15
+ - Modular scanner architecture under `src/scanners`
16
+ - Configurable behavior via `.securepushrc.json`
17
+ - Colored terminal output with file paths and line numbers
18
+ - JSON report mode for CI systems
19
+ - Git `pre-push` hook installation command
20
+
21
+ ## Project Structure
22
+
23
+ ```text
24
+ .
25
+ ├── bin
26
+ │ └── secure-push-check.js
27
+ ├── src
28
+ │ ├── scanners
29
+ │ │ ├── credentials.js
30
+ │ │ ├── deps.js
31
+ │ │ ├── files.js
32
+ │ │ ├── gitignore.js
33
+ │ │ └── secrets.js
34
+ │ ├── cli.js
35
+ │ └── index.js
36
+ ├── package.json
37
+ └── README.md
38
+ ```
39
+
40
+ ## Installation
41
+
42
+ ### Local project install
43
+
44
+ ```bash
45
+ npm install --save-dev secure-push-check
46
+ ```
47
+
48
+ ### Run from source
49
+
50
+ ```bash
51
+ npm install
52
+ npm run scan
53
+ ```
54
+
55
+ ## CLI Commands
56
+
57
+ ```bash
58
+ secure-push-check scan
59
+ secure-push-check install
60
+ secure-push-check report --json
61
+ ```
62
+
63
+ ### `scan`
64
+
65
+ Runs all checks and prints colorized output.
66
+
67
+ ```bash
68
+ secure-push-check scan
69
+ secure-push-check scan --json
70
+ secure-push-check scan --cwd /path/to/repo
71
+ ```
72
+
73
+ ### `report`
74
+
75
+ Runs all checks and prints report output (recommended with `--json` for automation).
76
+
77
+ ```bash
78
+ secure-push-check report --json
79
+ ```
80
+
81
+ ### `install`
82
+
83
+ Installs or updates a `pre-push` hook at `.git/hooks/pre-push`.
84
+
85
+ ```bash
86
+ secure-push-check install
87
+ ```
88
+
89
+ ## Configuration
90
+
91
+ Create `.securepushrc.json` in repository root:
92
+
93
+ ```json
94
+ {
95
+ "ignore": ["tests/**", "fixtures/**"],
96
+ "allowPatterns": ["TEST_KEY_", "/^fake_/i"],
97
+ "severity": "high",
98
+ "secretPatterns": [
99
+ {
100
+ "name": "Internal Token",
101
+ "regex": "int_[A-Za-z0-9]{24,}",
102
+ "flags": "g",
103
+ "severity": "critical"
104
+ }
105
+ ]
106
+ }
107
+ ```
108
+
109
+ ### Config Fields
110
+
111
+ - `ignore`: glob patterns excluded from file scanning.
112
+ - `allowPatterns`: strings or regex-like strings (`/pattern/flags`) to suppress known-safe matches.
113
+ - `severity`: push-block threshold (`low`, `moderate`, `high`, `critical`).
114
+ - `secretPatterns`: additional custom secret regex rules.
115
+
116
+ ## Output and Exit Codes
117
+
118
+ Report sections:
119
+ - `❌ Critical Issues`
120
+ - `⚠️ Warnings`
121
+ - `✅ Passed Checks`
122
+
123
+ Exit code semantics:
124
+ - `0`: safe to push
125
+ - `1`: push blocked
126
+
127
+ ## Example Output
128
+
129
+ ```text
130
+ 🔍 Secure Push Check v1.0.0
131
+
132
+ ❌ Critical Issues
133
+ - [CRITICAL] Potential OpenAI Key detected (src/config.js:18:16)
134
+ - [CRITICAL] Sensitive file exists and is not ignored by Git (.env)
135
+
136
+ ⚠️ Warnings
137
+ - [HIGH] Missing sensitive ignore pattern: .env.local (.gitignore)
138
+ - [MODERATE] npm audit found 2 moderate vulnerabilities. (package-lock.json)
139
+
140
+ ✅ Passed Checks
141
+ - Hardcoded Credentials Scan
142
+
143
+ Summary: critical=2, high=1, moderate=1, total=4
144
+ Result: BLOCKED (threshold: high)
145
+ ```
146
+
147
+ ## Git Hook Workflow
148
+
149
+ Install once:
150
+
151
+ ```bash
152
+ secure-push-check install
153
+ ```
154
+
155
+ During `git push`, the generated hook runs:
156
+
157
+ ```bash
158
+ secure-push-check scan
159
+ ```
160
+
161
+ If scan exits non-zero, push is blocked.
162
+
163
+ ## CI Usage
164
+
165
+ GitHub Actions example:
166
+
167
+ ```yaml
168
+ name: security-push-check
169
+ on:
170
+ pull_request:
171
+ push:
172
+ branches: [main]
173
+
174
+ jobs:
175
+ scan:
176
+ runs-on: ubuntu-latest
177
+ steps:
178
+ - uses: actions/checkout@v4
179
+ - uses: actions/setup-node@v4
180
+ with:
181
+ node-version: "20"
182
+ - run: npm ci
183
+ - run: npx secure-push-check report --json
184
+ ```
185
+
186
+ ## Publish Instructions
187
+
188
+ 1. Authenticate with npm:
189
+ ```bash
190
+ npm login
191
+ ```
192
+ 2. Verify package:
193
+ ```bash
194
+ npm pack --dry-run
195
+ ```
196
+ 3. Publish:
197
+ ```bash
198
+ npm publish --access public
199
+ ```
200
+ 4. Tag release:
201
+ ```bash
202
+ git tag v1.0.0
203
+ git push origin v1.0.0
204
+ ```
205
+
206
+ ## Contributing
207
+
208
+ 1. Fork and create a feature branch.
209
+ 2. Keep scanner modules focused and unit-testable.
210
+ 3. Add tests for parser/regex edge cases and hook installation flows.
211
+ 4. Open a PR with:
212
+ - problem statement
213
+ - change summary
214
+ - sample output
215
+
216
+ ## License
217
+
218
+ MIT
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+
3
+ import "../src/cli.js";
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "secure-push-check",
3
+ "version": "1.0.0",
4
+ "description": "Production-ready CLI that scans local Git repositories for security risks before push.",
5
+ "type": "module",
6
+ "main": "src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js"
9
+ },
10
+ "bin": {
11
+ "secure-push-check": "bin/secure-push-check.js"
12
+ },
13
+ "files": [
14
+ "bin",
15
+ "src",
16
+ "README.md"
17
+ ],
18
+ "engines": {
19
+ "node": ">=18"
20
+ },
21
+ "scripts": {
22
+ "scan": "node ./bin/secure-push-check.js scan",
23
+ "report:json": "node ./bin/secure-push-check.js report --json",
24
+ "install:hook": "node ./bin/secure-push-check.js install"
25
+ },
26
+ "keywords": [
27
+ "security",
28
+ "git",
29
+ "cli",
30
+ "secrets",
31
+ "audit",
32
+ "pre-push"
33
+ ],
34
+ "license": "MIT",
35
+ "dependencies": {
36
+ "@babel/parser": "^7.27.7",
37
+ "chalk": "^5.4.1",
38
+ "commander": "^12.1.0",
39
+ "fast-glob": "^3.3.3"
40
+ }
41
+ }
package/src/cli.js ADDED
@@ -0,0 +1,185 @@
1
+ import chalk from "chalk";
2
+ import { Command } from "commander";
3
+ import {
4
+ TOOL_VERSION,
5
+ installPrePushHook,
6
+ normalizeSeverity,
7
+ runScan
8
+ } from "./index.js";
9
+
10
+ /**
11
+ * @param {string} severity
12
+ * @returns {(text: string) => string}
13
+ */
14
+ function severityColor(severity) {
15
+ switch (normalizeSeverity(severity)) {
16
+ case "critical":
17
+ return chalk.red.bold;
18
+ case "high":
19
+ return chalk.yellow.bold;
20
+ case "moderate":
21
+ return chalk.hex("#f59e0b");
22
+ case "low":
23
+ return chalk.blue;
24
+ default:
25
+ return chalk.white;
26
+ }
27
+ }
28
+
29
+ /**
30
+ * @param {object} finding
31
+ * @returns {string}
32
+ */
33
+ function formatFindingLocation(finding) {
34
+ const base = finding.file || "unknown";
35
+ if (typeof finding.line === "number" && typeof finding.column === "number") {
36
+ return `${base}:${finding.line}:${finding.column}`;
37
+ }
38
+ if (typeof finding.line === "number") {
39
+ return `${base}:${finding.line}`;
40
+ }
41
+ return base;
42
+ }
43
+
44
+ /**
45
+ * @param {object} result
46
+ * @returns {void}
47
+ */
48
+ function renderHumanReport(result) {
49
+ const critical = [];
50
+ const warnings = [];
51
+
52
+ for (const check of result.checks) {
53
+ for (const finding of check.findings || []) {
54
+ const enriched = { ...finding, checkName: check.name };
55
+ if (normalizeSeverity(finding.severity) === "critical") {
56
+ critical.push(enriched);
57
+ } else {
58
+ warnings.push(enriched);
59
+ }
60
+ }
61
+ }
62
+
63
+ const passedChecks = result.checks.filter((check) => check.status === "passed");
64
+ const skippedChecks = result.checks.filter((check) => check.status === "skipped");
65
+
66
+ console.log(chalk.cyan.bold(`\n🔍 Secure Push Check v${result.version}\n`));
67
+
68
+ console.log(chalk.red.bold("❌ Critical Issues"));
69
+ if (critical.length === 0) {
70
+ console.log(chalk.green(" ✅ None"));
71
+ } else {
72
+ for (const finding of critical) {
73
+ const color = severityColor(finding.severity);
74
+ const severityTag = color(`[${String(finding.severity).toUpperCase()}]`);
75
+ console.log(
76
+ ` - ${severityTag} ${finding.message} (${formatFindingLocation(finding)})`
77
+ );
78
+ }
79
+ }
80
+
81
+ console.log(chalk.yellow.bold("\n⚠️ Warnings"));
82
+ if (warnings.length === 0) {
83
+ console.log(chalk.green(" ✅ None"));
84
+ } else {
85
+ for (const finding of warnings) {
86
+ const color = severityColor(finding.severity);
87
+ const severityTag = color(`[${String(finding.severity).toUpperCase()}]`);
88
+ console.log(
89
+ ` - ${severityTag} ${finding.message} (${formatFindingLocation(finding)})`
90
+ );
91
+ }
92
+ }
93
+
94
+ console.log(chalk.green.bold("\n✅ Passed Checks"));
95
+ if (passedChecks.length === 0) {
96
+ console.log(chalk.yellow(" - None"));
97
+ } else {
98
+ for (const check of passedChecks) {
99
+ console.log(chalk.green(` - ${check.name}`));
100
+ }
101
+ }
102
+
103
+ if (skippedChecks.length > 0) {
104
+ console.log(chalk.gray.bold("\nℹ️ Skipped Checks"));
105
+ for (const check of skippedChecks) {
106
+ console.log(chalk.gray(` - ${check.name}`));
107
+ }
108
+ }
109
+
110
+ console.log(
111
+ chalk.bold(
112
+ `\nSummary: critical=${result.summary.critical}, high=${result.summary.high}, moderate=${result.summary.moderate}, total=${result.summary.totalFindings}`
113
+ )
114
+ );
115
+ console.log(
116
+ result.summary.blocked
117
+ ? chalk.red(`Result: BLOCKED (threshold: ${result.summary.severityThreshold})`)
118
+ : chalk.green(`Result: SAFE (threshold: ${result.summary.severityThreshold})`)
119
+ );
120
+ }
121
+
122
+ /**
123
+ * @param {object} options
124
+ * @param {boolean} options.json
125
+ * @param {string} options.cwd
126
+ * @returns {Promise<void>}
127
+ */
128
+ async function executeScan(options) {
129
+ const result = await runScan({ cwd: options.cwd });
130
+
131
+ if (options.json) {
132
+ console.log(JSON.stringify(result, null, 2));
133
+ } else {
134
+ renderHumanReport(result);
135
+ }
136
+
137
+ process.exitCode = result.summary.blocked ? 1 : 0;
138
+ }
139
+
140
+ const program = new Command();
141
+ program
142
+ .name("secure-push-check")
143
+ .description("Scan local Git repositories for security risks before pushing.")
144
+ .version(TOOL_VERSION)
145
+ .showHelpAfterError();
146
+
147
+ program
148
+ .command("scan")
149
+ .description("Run all checks and print a colorized report.")
150
+ .option("--json", "Output report as JSON.")
151
+ .option("--cwd <path>", "Working directory to scan.", process.cwd())
152
+ .action(async (options) => {
153
+ await executeScan({
154
+ json: Boolean(options.json),
155
+ cwd: options.cwd
156
+ });
157
+ });
158
+
159
+ program
160
+ .command("report")
161
+ .description("Run all checks and generate a report.")
162
+ .option("--json", "Output report as JSON.")
163
+ .option("--cwd <path>", "Working directory to scan.", process.cwd())
164
+ .action(async (options) => {
165
+ await executeScan({
166
+ json: Boolean(options.json),
167
+ cwd: options.cwd
168
+ });
169
+ });
170
+
171
+ program
172
+ .command("install")
173
+ .description("Install secure-push-check as a pre-push Git hook.")
174
+ .option("--cwd <path>", "Repository path where hook should be installed.", process.cwd())
175
+ .action(async (options) => {
176
+ const result = await installPrePushHook({ cwd: options.cwd });
177
+ console.log(chalk.green(`Pre-push hook ${result.operation} at ${result.hookPath}`));
178
+ });
179
+
180
+ try {
181
+ await program.parseAsync(process.argv);
182
+ } catch (error) {
183
+ console.error(chalk.red(`Error: ${error.message}`));
184
+ process.exitCode = 1;
185
+ }
package/src/index.js ADDED
@@ -0,0 +1,266 @@
1
+ import path from "node:path";
2
+ import { execFile } from "node:child_process";
3
+ import { promises as fs } from "node:fs";
4
+ import { createRequire } from "node:module";
5
+ import { promisify } from "node:util";
6
+ import { scanSecrets } from "./scanners/secrets.js";
7
+ import { scanSensitiveFiles } from "./scanners/files.js";
8
+ import { validateGitignore } from "./scanners/gitignore.js";
9
+ import { scanDependencies } from "./scanners/deps.js";
10
+ import { scanHardcodedCredentials } from "./scanners/credentials.js";
11
+
12
+ const execFileAsync = promisify(execFile);
13
+ const require = createRequire(import.meta.url);
14
+ const pkg = require("../package.json");
15
+
16
+ const HOOK_MARKER_START = "# secure-push-check hook start";
17
+ const HOOK_MARKER_END = "# secure-push-check hook end";
18
+
19
+ const SEVERITY_RANK = {
20
+ low: 1,
21
+ moderate: 2,
22
+ high: 3,
23
+ critical: 4
24
+ };
25
+
26
+ export const TOOL_NAME = "secure-push-check";
27
+ export const TOOL_VERSION = pkg.version;
28
+ export const DEFAULT_CONFIG = Object.freeze({
29
+ ignore: [],
30
+ allowPatterns: [],
31
+ severity: "high",
32
+ secretPatterns: []
33
+ });
34
+
35
+ /**
36
+ * @param {unknown} severity
37
+ * @returns {"low" | "moderate" | "high" | "critical"}
38
+ */
39
+ export function normalizeSeverity(severity) {
40
+ const value = String(severity || "").trim().toLowerCase();
41
+ if (value in SEVERITY_RANK) {
42
+ return /** @type {"low" | "moderate" | "high" | "critical"} */ (value);
43
+ }
44
+ return "high";
45
+ }
46
+
47
+ /**
48
+ * @param {unknown} values
49
+ * @returns {string[]}
50
+ */
51
+ function uniqueStringList(values) {
52
+ if (!Array.isArray(values)) {
53
+ return [];
54
+ }
55
+
56
+ return [...new Set(values.filter((item) => typeof item === "string" && item.trim() !== "").map((item) => item.trim()))];
57
+ }
58
+
59
+ /**
60
+ * @param {string} repoRoot
61
+ * @returns {Promise<object>}
62
+ */
63
+ export async function loadConfig(repoRoot) {
64
+ const configPath = path.join(repoRoot, ".securepushrc.json");
65
+ let parsed = {};
66
+
67
+ try {
68
+ const content = await fs.readFile(configPath, "utf8");
69
+ parsed = JSON.parse(content);
70
+ } catch (error) {
71
+ if (error.code !== "ENOENT") {
72
+ throw new Error(`Failed to parse .securepushrc.json: ${error.message}`);
73
+ }
74
+ }
75
+
76
+ return {
77
+ ignore: uniqueStringList(parsed.ignore),
78
+ allowPatterns: uniqueStringList(parsed.allowPatterns),
79
+ severity: normalizeSeverity(parsed.severity || DEFAULT_CONFIG.severity),
80
+ secretPatterns: Array.isArray(parsed.secretPatterns) ? parsed.secretPatterns : [],
81
+ configPath
82
+ };
83
+ }
84
+
85
+ /**
86
+ * @param {string} cwd
87
+ * @returns {Promise<string>}
88
+ */
89
+ export async function findGitRepositoryRoot(cwd = process.cwd()) {
90
+ try {
91
+ const { stdout } = await execFileAsync("git", ["rev-parse", "--show-toplevel"], { cwd });
92
+ const repoRoot = stdout.trim();
93
+ if (!repoRoot) {
94
+ throw new Error("Git returned an empty repository path.");
95
+ }
96
+ return repoRoot;
97
+ } catch (error) {
98
+ throw new Error(`Unable to resolve Git repository root from '${cwd}': ${error.message}`);
99
+ }
100
+ }
101
+
102
+ /**
103
+ * @param {Array<object>} checks
104
+ * @param {string} severityThreshold
105
+ * @returns {object}
106
+ */
107
+ export function createSummary(checks, severityThreshold) {
108
+ const findings = checks.flatMap((check) => (Array.isArray(check.findings) ? check.findings : []));
109
+ const severityCounts = {
110
+ critical: 0,
111
+ high: 0,
112
+ moderate: 0,
113
+ low: 0
114
+ };
115
+
116
+ for (const finding of findings) {
117
+ const severity = normalizeSeverity(finding.severity);
118
+ severityCounts[severity] += 1;
119
+ }
120
+
121
+ const threshold = normalizeSeverity(severityThreshold);
122
+ const blocked = findings.some((finding) => SEVERITY_RANK[normalizeSeverity(finding.severity)] >= SEVERITY_RANK[threshold]);
123
+
124
+ return {
125
+ ...severityCounts,
126
+ totalFindings: findings.length,
127
+ passedChecks: checks.filter((check) => check.status === "passed").length,
128
+ failedChecks: checks.filter((check) => check.status === "failed").length,
129
+ skippedChecks: checks.filter((check) => check.status === "skipped").length,
130
+ severityThreshold: threshold,
131
+ blocked
132
+ };
133
+ }
134
+
135
+ /**
136
+ * @param {object} options
137
+ * @param {string} [options.cwd]
138
+ * @returns {Promise<object>}
139
+ */
140
+ export async function runScan(options = {}) {
141
+ const cwd = options.cwd || process.cwd();
142
+ const repoRoot = await findGitRepositoryRoot(cwd);
143
+ const config = await loadConfig(repoRoot);
144
+
145
+ const scannerOptions = {
146
+ repoRoot,
147
+ ignoreGlobs: config.ignore,
148
+ allowPatterns: config.allowPatterns,
149
+ secretPatterns: config.secretPatterns
150
+ };
151
+
152
+ const checks = await Promise.all([
153
+ scanSecrets(scannerOptions),
154
+ scanSensitiveFiles(scannerOptions),
155
+ validateGitignore(scannerOptions),
156
+ scanDependencies(scannerOptions),
157
+ scanHardcodedCredentials(scannerOptions)
158
+ ]);
159
+
160
+ const summary = createSummary(checks, config.severity);
161
+
162
+ return {
163
+ tool: TOOL_NAME,
164
+ version: TOOL_VERSION,
165
+ scannedAt: new Date().toISOString(),
166
+ repository: repoRoot,
167
+ config: {
168
+ ignore: config.ignore,
169
+ allowPatterns: config.allowPatterns,
170
+ severity: config.severity,
171
+ customSecretPatterns: config.secretPatterns.length
172
+ },
173
+ checks,
174
+ summary
175
+ };
176
+ }
177
+
178
+ /**
179
+ * @returns {string}
180
+ */
181
+ function createHookSnippet() {
182
+ return [
183
+ HOOK_MARKER_START,
184
+ "if command -v secure-push-check >/dev/null 2>&1; then",
185
+ " secure-push-check scan",
186
+ " SECURE_PUSH_CHECK_STATUS=$?",
187
+ "else",
188
+ " npx --no-install secure-push-check scan",
189
+ " SECURE_PUSH_CHECK_STATUS=$?",
190
+ "fi",
191
+ "if [ $SECURE_PUSH_CHECK_STATUS -ne 0 ]; then",
192
+ " echo \"secure-push-check blocked this push.\"",
193
+ " exit $SECURE_PUSH_CHECK_STATUS",
194
+ "fi",
195
+ HOOK_MARKER_END
196
+ ].join("\n");
197
+ }
198
+
199
+ /**
200
+ * @param {string} existingContent
201
+ * @returns {string}
202
+ */
203
+ function upsertHookContent(existingContent) {
204
+ const snippet = createHookSnippet();
205
+ const content = existingContent || "";
206
+
207
+ if (content.trim() === "") {
208
+ return `#!/bin/sh\n\n${snippet}\n`;
209
+ }
210
+
211
+ if (content.includes(HOOK_MARKER_START) && content.includes(HOOK_MARKER_END)) {
212
+ const escapedStart = HOOK_MARKER_START.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
213
+ const escapedEnd = HOOK_MARKER_END.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
214
+ const blockRegex = new RegExp(`${escapedStart}[\\s\\S]*?${escapedEnd}\\n?`, "g");
215
+ return content.replace(blockRegex, `${snippet}\n`);
216
+ }
217
+
218
+ const separator = content.endsWith("\n") ? "\n" : "\n\n";
219
+ return `${content}${separator}${snippet}\n`;
220
+ }
221
+
222
+ /**
223
+ * Install or update a pre-push Git hook.
224
+ *
225
+ * @param {object} options
226
+ * @param {string} [options.cwd]
227
+ * @returns {Promise<object>}
228
+ */
229
+ export async function installPrePushHook(options = {}) {
230
+ const cwd = options.cwd || process.cwd();
231
+ const repoRoot = await findGitRepositoryRoot(cwd);
232
+ const hooksDir = path.join(repoRoot, ".git", "hooks");
233
+ const hookPath = path.join(hooksDir, "pre-push");
234
+
235
+ await fs.mkdir(hooksDir, { recursive: true });
236
+
237
+ let existingContent = "";
238
+ try {
239
+ existingContent = await fs.readFile(hookPath, "utf8");
240
+ } catch (error) {
241
+ if (error.code !== "ENOENT") {
242
+ throw error;
243
+ }
244
+ }
245
+
246
+ const nextContent = upsertHookContent(existingContent);
247
+ const operation = existingContent
248
+ ? existingContent.includes(HOOK_MARKER_START)
249
+ ? "updated"
250
+ : "appended"
251
+ : "created";
252
+
253
+ await fs.writeFile(hookPath, nextContent, "utf8");
254
+
255
+ try {
256
+ await fs.chmod(hookPath, 0o755);
257
+ } catch {
258
+ // Windows filesystems may ignore chmod, which is safe for this workflow.
259
+ }
260
+
261
+ return {
262
+ repository: repoRoot,
263
+ hookPath,
264
+ operation
265
+ };
266
+ }