calllint 0.9.1 → 0.9.3
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/dist/index.js +258 -14
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4473,7 +4473,7 @@ function receiptCommand(args, deps) {
|
|
|
4473
4473
|
}
|
|
4474
4474
|
|
|
4475
4475
|
// src/commands/action.ts
|
|
4476
|
-
import { readFileSync as readFileSync9 } from "node:fs";
|
|
4476
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync6 } from "node:fs";
|
|
4477
4477
|
import { resolve as resolve5 } from "node:path";
|
|
4478
4478
|
|
|
4479
4479
|
// ../../packages/action-analyzer/src/types.ts
|
|
@@ -4669,18 +4669,221 @@ function analyzePayment(descriptor) {
|
|
|
4669
4669
|
}
|
|
4670
4670
|
function analyzeRegistration(descriptor) {
|
|
4671
4671
|
const findings = [];
|
|
4672
|
+
const metadata = descriptor.metadata || {};
|
|
4673
|
+
const serviceVerified = metadata.service_verified;
|
|
4674
|
+
if (serviceVerified === false) {
|
|
4675
|
+
findings.push({
|
|
4676
|
+
id: "action.unverified-service",
|
|
4677
|
+
title: "Unverified service registration",
|
|
4678
|
+
severity: "high",
|
|
4679
|
+
blocker: false,
|
|
4680
|
+
symbol: "SUPPLY",
|
|
4681
|
+
riskClass: "S3",
|
|
4682
|
+
mode: "OBSERVED",
|
|
4683
|
+
confidence: "high",
|
|
4684
|
+
detectionMethod: "config-analysis",
|
|
4685
|
+
evidence: [{
|
|
4686
|
+
type: "config",
|
|
4687
|
+
path: "metadata.service_verified",
|
|
4688
|
+
value: "false"
|
|
4689
|
+
}],
|
|
4690
|
+
impact: "Account registration on unverified service",
|
|
4691
|
+
fix: "Verify the service domain and reputation before registering"
|
|
4692
|
+
});
|
|
4693
|
+
}
|
|
4694
|
+
const scopes = metadata.oauth_scopes || [];
|
|
4695
|
+
const dangerousScopes = ["admin", "delete_account", "financial_data", "admin:org"];
|
|
4696
|
+
const foundDangerous = scopes.filter(
|
|
4697
|
+
(s) => dangerousScopes.some((d) => s.toLowerCase().includes(d.toLowerCase()))
|
|
4698
|
+
);
|
|
4699
|
+
if (foundDangerous.length > 0) {
|
|
4700
|
+
findings.push({
|
|
4701
|
+
id: "action.excessive-oauth-scopes",
|
|
4702
|
+
title: "Excessive OAuth scopes requested",
|
|
4703
|
+
severity: "medium",
|
|
4704
|
+
blocker: false,
|
|
4705
|
+
symbol: "SECRETS",
|
|
4706
|
+
riskClass: "S2",
|
|
4707
|
+
mode: "OBSERVED",
|
|
4708
|
+
confidence: "high",
|
|
4709
|
+
detectionMethod: "config-analysis",
|
|
4710
|
+
evidence: [{
|
|
4711
|
+
type: "config",
|
|
4712
|
+
path: "metadata.oauth_scopes",
|
|
4713
|
+
value: foundDangerous.join(", ")
|
|
4714
|
+
}],
|
|
4715
|
+
impact: `Registration requests sensitive scopes: ${foundDangerous.join(", ")}`,
|
|
4716
|
+
fix: "Request only the minimum necessary OAuth scopes"
|
|
4717
|
+
});
|
|
4718
|
+
}
|
|
4672
4719
|
return findings;
|
|
4673
4720
|
}
|
|
4674
4721
|
function analyzeGitHub(descriptor) {
|
|
4675
4722
|
const findings = [];
|
|
4723
|
+
const metadata = descriptor.metadata || {};
|
|
4724
|
+
const repoVerified = metadata.repository_verified;
|
|
4725
|
+
if (repoVerified === false) {
|
|
4726
|
+
findings.push({
|
|
4727
|
+
id: "action.unverified-repository",
|
|
4728
|
+
title: "Unverified repository target",
|
|
4729
|
+
severity: "medium",
|
|
4730
|
+
blocker: false,
|
|
4731
|
+
symbol: "SUPPLY",
|
|
4732
|
+
riskClass: "S3",
|
|
4733
|
+
mode: "OBSERVED",
|
|
4734
|
+
confidence: "high",
|
|
4735
|
+
detectionMethod: "config-analysis",
|
|
4736
|
+
evidence: [{
|
|
4737
|
+
type: "config",
|
|
4738
|
+
path: "metadata.repository_verified",
|
|
4739
|
+
value: "false"
|
|
4740
|
+
}],
|
|
4741
|
+
impact: "GitHub write operation to unverified repository",
|
|
4742
|
+
fix: "Verify repository ownership before writing"
|
|
4743
|
+
});
|
|
4744
|
+
}
|
|
4745
|
+
const scopes = metadata.oauth_scopes || [];
|
|
4746
|
+
const dangerousScopes = ["delete_repo", "admin:org", "admin:repo_hook"];
|
|
4747
|
+
const foundDangerous = scopes.filter(
|
|
4748
|
+
(s) => dangerousScopes.some((d) => s.toLowerCase().includes(d.toLowerCase()))
|
|
4749
|
+
);
|
|
4750
|
+
if (foundDangerous.length > 0) {
|
|
4751
|
+
findings.push({
|
|
4752
|
+
id: "action.excessive-github-scopes",
|
|
4753
|
+
title: "Excessive GitHub OAuth scopes",
|
|
4754
|
+
severity: "medium",
|
|
4755
|
+
blocker: false,
|
|
4756
|
+
symbol: "SECRETS",
|
|
4757
|
+
riskClass: "S3",
|
|
4758
|
+
mode: "OBSERVED",
|
|
4759
|
+
confidence: "high",
|
|
4760
|
+
detectionMethod: "config-analysis",
|
|
4761
|
+
evidence: [{
|
|
4762
|
+
type: "config",
|
|
4763
|
+
path: "metadata.oauth_scopes",
|
|
4764
|
+
value: foundDangerous.join(", ")
|
|
4765
|
+
}],
|
|
4766
|
+
impact: `GitHub operation requests dangerous scopes: ${foundDangerous.join(", ")}`,
|
|
4767
|
+
fix: "Request only necessary OAuth scopes for the operation"
|
|
4768
|
+
});
|
|
4769
|
+
}
|
|
4770
|
+
const externalLinks = metadata.external_links || [];
|
|
4771
|
+
if (externalLinks.length > 0) {
|
|
4772
|
+
findings.push({
|
|
4773
|
+
id: "action.external-links",
|
|
4774
|
+
title: "External links in GitHub content",
|
|
4775
|
+
severity: "low",
|
|
4776
|
+
blocker: false,
|
|
4777
|
+
symbol: "NETWORK",
|
|
4778
|
+
riskClass: "S2",
|
|
4779
|
+
mode: "OBSERVED",
|
|
4780
|
+
confidence: "medium",
|
|
4781
|
+
detectionMethod: "config-analysis",
|
|
4782
|
+
evidence: [{
|
|
4783
|
+
type: "config",
|
|
4784
|
+
path: "metadata.external_links",
|
|
4785
|
+
value: externalLinks.join(", ")
|
|
4786
|
+
}],
|
|
4787
|
+
impact: `Issue/PR contains external links: ${externalLinks.length} found`,
|
|
4788
|
+
fix: "Review external links for safety"
|
|
4789
|
+
});
|
|
4790
|
+
}
|
|
4676
4791
|
return findings;
|
|
4677
4792
|
}
|
|
4678
4793
|
function analyzeNpmPublish(descriptor) {
|
|
4679
4794
|
const findings = [];
|
|
4795
|
+
const metadata = descriptor.metadata || {};
|
|
4796
|
+
const params = descriptor.parameters || {};
|
|
4797
|
+
const similarTo = metadata.similar_to_popular || [];
|
|
4798
|
+
if (similarTo.length > 0) {
|
|
4799
|
+
findings.push({
|
|
4800
|
+
id: "supply.name-squatting",
|
|
4801
|
+
title: "Potential name squatting",
|
|
4802
|
+
severity: "high",
|
|
4803
|
+
blocker: false,
|
|
4804
|
+
symbol: "SUPPLY",
|
|
4805
|
+
riskClass: "S3",
|
|
4806
|
+
mode: "INFERRED",
|
|
4807
|
+
confidence: "medium",
|
|
4808
|
+
detectionMethod: "config-analysis",
|
|
4809
|
+
evidence: [{
|
|
4810
|
+
type: "config",
|
|
4811
|
+
path: "metadata.similar_to_popular",
|
|
4812
|
+
value: similarTo.join(", ")
|
|
4813
|
+
}],
|
|
4814
|
+
impact: `Package name similar to popular packages: ${similarTo.join(", ")}`,
|
|
4815
|
+
fix: "Verify this is not a typosquatting attempt"
|
|
4816
|
+
});
|
|
4817
|
+
}
|
|
4818
|
+
const versionPinned = metadata.version_pinned;
|
|
4819
|
+
const versionRange = metadata.version_range;
|
|
4820
|
+
if (versionPinned === false || versionRange === true) {
|
|
4821
|
+
findings.push({
|
|
4822
|
+
id: "supply.version-float",
|
|
4823
|
+
title: "Version not pinned",
|
|
4824
|
+
severity: "medium",
|
|
4825
|
+
blocker: false,
|
|
4826
|
+
symbol: "SUPPLY",
|
|
4827
|
+
riskClass: "S2",
|
|
4828
|
+
mode: "OBSERVED",
|
|
4829
|
+
confidence: "high",
|
|
4830
|
+
detectionMethod: "config-analysis",
|
|
4831
|
+
evidence: [{
|
|
4832
|
+
type: "config",
|
|
4833
|
+
path: "parameters.version",
|
|
4834
|
+
value: String(params.version || "unknown")
|
|
4835
|
+
}],
|
|
4836
|
+
impact: "Publishing with floating version range instead of pinned version",
|
|
4837
|
+
fix: 'Use pinned version (e.g., "1.2.3" not "^1.2.3")'
|
|
4838
|
+
});
|
|
4839
|
+
}
|
|
4680
4840
|
return findings;
|
|
4681
4841
|
}
|
|
4682
4842
|
function analyzeCloud(descriptor) {
|
|
4683
4843
|
const findings = [];
|
|
4844
|
+
const metadata = descriptor.metadata || {};
|
|
4845
|
+
const monthlyCost = metadata.estimated_monthly_cost;
|
|
4846
|
+
if (typeof monthlyCost === "number" && monthlyCost > 1e3) {
|
|
4847
|
+
findings.push({
|
|
4848
|
+
id: "action.expensive-cloud-resource",
|
|
4849
|
+
title: "Expensive cloud resource",
|
|
4850
|
+
severity: "high",
|
|
4851
|
+
blocker: false,
|
|
4852
|
+
symbol: "MONEY",
|
|
4853
|
+
riskClass: "S5",
|
|
4854
|
+
mode: "OBSERVED",
|
|
4855
|
+
confidence: "high",
|
|
4856
|
+
detectionMethod: "config-analysis",
|
|
4857
|
+
evidence: [{
|
|
4858
|
+
type: "config",
|
|
4859
|
+
path: "metadata.estimated_monthly_cost",
|
|
4860
|
+
value: String(monthlyCost)
|
|
4861
|
+
}],
|
|
4862
|
+
impact: `Cloud resource estimated at $${monthlyCost}/month`,
|
|
4863
|
+
fix: "Verify cost estimate and budget before creating"
|
|
4864
|
+
});
|
|
4865
|
+
}
|
|
4866
|
+
const opensAllPorts = metadata.opens_all_ports;
|
|
4867
|
+
if (opensAllPorts === true) {
|
|
4868
|
+
findings.push({
|
|
4869
|
+
id: "action.insecure-security-group",
|
|
4870
|
+
title: "Insecure security group configuration",
|
|
4871
|
+
severity: "high",
|
|
4872
|
+
blocker: false,
|
|
4873
|
+
symbol: "NETWORK",
|
|
4874
|
+
riskClass: "S4",
|
|
4875
|
+
mode: "OBSERVED",
|
|
4876
|
+
confidence: "high",
|
|
4877
|
+
detectionMethod: "config-analysis",
|
|
4878
|
+
evidence: [{
|
|
4879
|
+
type: "config",
|
|
4880
|
+
path: "metadata.opens_all_ports",
|
|
4881
|
+
value: "true"
|
|
4882
|
+
}],
|
|
4883
|
+
impact: "Security group opens all ports to the internet (0.0.0.0/0)",
|
|
4884
|
+
fix: "Restrict ports and source IPs to minimum necessary"
|
|
4885
|
+
});
|
|
4886
|
+
}
|
|
4684
4887
|
return findings;
|
|
4685
4888
|
}
|
|
4686
4889
|
function analyzeSecretShapedHeaders(descriptor) {
|
|
@@ -4760,21 +4963,28 @@ Expected: calllint.action.v0`,
|
|
|
4760
4963
|
const policy = loadPolicyOrDefault(policyPath ? resolve5(deps.cwd, policyPath) : void 0);
|
|
4761
4964
|
const findings = analyzeAction(descriptor);
|
|
4762
4965
|
const verdict = computeActionVerdict(findings, policy);
|
|
4966
|
+
const actionReport = {
|
|
4967
|
+
schema_version: "calllint.action-report.v0",
|
|
4968
|
+
verdict,
|
|
4969
|
+
findings,
|
|
4970
|
+
target: {
|
|
4971
|
+
type: "action",
|
|
4972
|
+
kind: descriptor.kind,
|
|
4973
|
+
schema_version: descriptor.schema_version
|
|
4974
|
+
},
|
|
4975
|
+
policy_applied: "default",
|
|
4976
|
+
scan_timestamp: deps.generatedAt || (/* @__PURE__ */ new Date()).toISOString(),
|
|
4977
|
+
counts: {
|
|
4978
|
+
SAFE: findings.filter((f) => !f.blocker && f.severity !== "high").length === 0 ? 1 : 0,
|
|
4979
|
+
REVIEW: findings.some((f) => f.severity === "high") ? 1 : 0,
|
|
4980
|
+
BLOCK: findings.some((f) => f.blocker || f.severity === "critical") ? 1 : 0,
|
|
4981
|
+
UNKNOWN: 0
|
|
4982
|
+
},
|
|
4983
|
+
reports: [{ findings }]
|
|
4984
|
+
};
|
|
4763
4985
|
let stdout = "";
|
|
4764
4986
|
if (args.flags["json"]) {
|
|
4765
|
-
|
|
4766
|
-
schema_version: "calllint.action-report.v0",
|
|
4767
|
-
verdict,
|
|
4768
|
-
findings,
|
|
4769
|
-
target: {
|
|
4770
|
-
type: "action",
|
|
4771
|
-
kind: descriptor.kind,
|
|
4772
|
-
schema_version: descriptor.schema_version
|
|
4773
|
-
},
|
|
4774
|
-
policy_applied: "default",
|
|
4775
|
-
scan_timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
4776
|
-
};
|
|
4777
|
-
stdout = JSON.stringify(report, null, 2);
|
|
4987
|
+
stdout = JSON.stringify(actionReport, null, 2);
|
|
4778
4988
|
} else {
|
|
4779
4989
|
const header = `
|
|
4780
4990
|
\u{1F50D} Action: ${descriptor.kind}
|
|
@@ -4794,6 +5004,12 @@ Expected: calllint.action.v0`,
|
|
|
4794
5004
|
}
|
|
4795
5005
|
stdout = header + body;
|
|
4796
5006
|
}
|
|
5007
|
+
if (args.flags["receipt"]) {
|
|
5008
|
+
const receiptError = writeActionReceipt(descriptor, content, actionReport, policy, args, deps);
|
|
5009
|
+
if (receiptError) {
|
|
5010
|
+
return { stdout: "", stderr: receiptError, exitCode: 2 };
|
|
5011
|
+
}
|
|
5012
|
+
}
|
|
4797
5013
|
return {
|
|
4798
5014
|
stdout,
|
|
4799
5015
|
stderr: "",
|
|
@@ -4831,11 +5047,14 @@ COMMANDS
|
|
|
4831
5047
|
OPTIONS
|
|
4832
5048
|
--json Output JSON report
|
|
4833
5049
|
--policy <file> Use custom policy file
|
|
5050
|
+
--receipt Generate a calllint.receipt.v0 file
|
|
5051
|
+
--receipt-out <file> Receipt output path (default: calllint-action-receipt.json)
|
|
4834
5052
|
--no-emoji Disable emoji in output
|
|
4835
5053
|
|
|
4836
5054
|
EXAMPLE
|
|
4837
5055
|
calllint action inspect payment.json
|
|
4838
5056
|
calllint action inspect email-reply.json --json
|
|
5057
|
+
calllint action inspect payment.json --receipt
|
|
4839
5058
|
|
|
4840
5059
|
See: https://calllint.com/docs/action-inspect (ADR 0029)
|
|
4841
5060
|
`;
|
|
@@ -4854,6 +5073,31 @@ function computeActionVerdict(findings, policy) {
|
|
|
4854
5073
|
}
|
|
4855
5074
|
return "REVIEW";
|
|
4856
5075
|
}
|
|
5076
|
+
function writeActionReceipt(descriptor, rawContent, actionReport, policy, args, deps) {
|
|
5077
|
+
const toolVersion = deps.toolVersion ?? "0.0.0-dev";
|
|
5078
|
+
const receipt = createReceipt(
|
|
5079
|
+
{
|
|
5080
|
+
toolVersion,
|
|
5081
|
+
subject: { type: "action", target: descriptor.kind },
|
|
5082
|
+
inputForHash: rawContent,
|
|
5083
|
+
effectivePolicyForHash: policy ?? { policy: "default" },
|
|
5084
|
+
scanReport: actionReport,
|
|
5085
|
+
rulesetForHash: { tool: "calllint", version: toolVersion },
|
|
5086
|
+
networkUsed: false
|
|
5087
|
+
},
|
|
5088
|
+
deps.generatedAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
5089
|
+
);
|
|
5090
|
+
const outPath = resolve5(
|
|
5091
|
+
deps.cwd,
|
|
5092
|
+
args.flags["receipt-out"] ?? "calllint-action-receipt.json"
|
|
5093
|
+
);
|
|
5094
|
+
try {
|
|
5095
|
+
writeFileSync6(outPath, JSON.stringify(receipt, null, 2) + "\n", "utf8");
|
|
5096
|
+
} catch (e) {
|
|
5097
|
+
return `Could not write action receipt to ${outPath}: ${e instanceof Error ? e.message : String(e)}`;
|
|
5098
|
+
}
|
|
5099
|
+
return void 0;
|
|
5100
|
+
}
|
|
4857
5101
|
|
|
4858
5102
|
// src/run.ts
|
|
4859
5103
|
function run(argv, deps) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "calllint",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Evidence-backed security verdicts for MCP servers and agent tools. Lint agent tool-call risk before tools run — SAFE / REVIEW / BLOCK / UNKNOWN, with evidence. Never executes the server it judges.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|